From 1205526422d82956c197b08932a41b2162c52c7d Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 2 Oct 2026 21:31:03 -0400 Subject: [PATCH] feat(notes): raise annotation text limit from 10,000 to 100,000 chars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 10k validator cap rejected legitimate long-form notes — a scholarly reading note with quoted passages and footnotes lands around 10.1k chars and failed at save with the raw validator error. Raise the cap to 100,000 on all six annotation text fields in the media handler request structs: - CreateMediaNoteRequest / UpdateMediaNoteRequest Content - CreateMediaHighlightRequest / UpdateMediaHighlightRequest NoteText - CreateMediaBookmarkRequest / UpdateMediaBookmarkRequest Notes No other layer changes: the media_notes / media_highlights columns are unbounded TEXT, and the KOReader + websocket sync paths never had a length cap, so the REST API now matches the rest of the system instead of being the strictest gate. New integration test pins the boundary: 50k and 100k-char notes return 201, 100,001 chars returns 400. --- cmd/server/tests/media_note_length_test.go | 54 ++++++++++++++++++++++ internal/handlers/media.go | 12 ++--- 2 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 cmd/server/tests/media_note_length_test.go diff --git a/cmd/server/tests/media_note_length_test.go b/cmd/server/tests/media_note_length_test.go new file mode 100644 index 0000000..3447af9 --- /dev/null +++ b/cmd/server/tests/media_note_length_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMediaNoteLengthLimits verifies the note content validator cap +// (internal/handlers/media.go): content up to 100,000 chars is accepted, +// longer content is rejected with 400. +func TestMediaNoteLengthLimits(t *testing.T) { + setup := setupTestServer(t) + + token := setup.Token + mediaItemID := createTestMediaItemID(t, setup) + + postNote := func(content string) int { + payload := map[string]interface{}{ + "content": content, + "position": "page:1", + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items/"+mediaItemID+"/notes", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + return resp.StatusCode + } + + t.Run("50k-char note accepted", func(t *testing.T) { + assert.Equal(t, http.StatusCreated, postNote(strings.Repeat("a", 50000))) + }) + + t.Run("100k-char note accepted at boundary", func(t *testing.T) { + assert.Equal(t, http.StatusCreated, postNote(strings.Repeat("b", 100000))) + }) + + t.Run("over-limit note rejected", func(t *testing.T) { + assert.Equal(t, http.StatusBadRequest, postNote(strings.Repeat("c", 100001))) + }) +} diff --git a/internal/handlers/media.go b/internal/handlers/media.go index e2aa501..60a3cc0 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -102,13 +102,13 @@ type UpdateMediaItemRequest struct { // CreateMediaNoteRequest represents the request for creating a media note type CreateMediaNoteRequest struct { - Content string `json:"content" validate:"required,min=1,max=10000"` + Content string `json:"content" validate:"required,min=1,max=100000"` Position string `json:"position"` } // UpdateMediaNoteRequest represents the request for updating a media note type UpdateMediaNoteRequest struct { - Content string `json:"content" validate:"required,min=1,max=10000"` + Content string `json:"content" validate:"required,min=1,max=100000"` Position string `json:"position"` } @@ -120,7 +120,7 @@ type CreateMediaHighlightRequest struct { EpubcfiStart string `json:"epubcfi_start" validate:"max=2000"` EpubcfiEnd string `json:"epubcfi_end" validate:"max=2000"` Color string `json:"color" validate:"omitempty,len=7"` - NoteText string `json:"note_text" validate:"max=10000"` + NoteText string `json:"note_text" validate:"max=100000"` NoteID string `json:"note_id"` PercentageStart float64 `json:"percentage_start"` PercentageEnd float64 `json:"percentage_end"` @@ -135,7 +135,7 @@ type UpdateMediaHighlightRequest struct { EpubcfiStart string `json:"epubcfi_start" validate:"max=2000"` EpubcfiEnd string `json:"epubcfi_end" validate:"max=2000"` Color string `json:"color" validate:"omitempty,len=7"` - NoteText string `json:"note_text" validate:"max=10000"` + NoteText string `json:"note_text" validate:"max=100000"` NoteID string `json:"note_id"` PercentageStart float64 `json:"percentage_start"` PercentageEnd float64 `json:"percentage_end"` @@ -146,7 +146,7 @@ type UpdateMediaHighlightRequest struct { type CreateMediaBookmarkRequest struct { Title string `json:"title" validate:"required,min=1,max=255"` Position string `json:"position" validate:"max=100"` - Notes string `json:"notes" validate:"max=10000"` + Notes string `json:"notes" validate:"max=100000"` CfiPosition string `json:"cfi_position" validate:"max=255"` PageNumber int32 `json:"page_number"` ChapterNumber int32 `json:"chapter_number"` @@ -160,7 +160,7 @@ type CreateMediaBookmarkRequest struct { // UpdateMediaBookmarkRequest represents the request for updating a media bookmark type UpdateMediaBookmarkRequest struct { Title string `json:"title" validate:"required,min=1,max=255"` - Notes string `json:"notes" validate:"max=10000"` + Notes string `json:"notes" validate:"max=100000"` Position string `json:"position" validate:"max=100"` }