From 859e6a8b09173675a0503f5c5dc4cfc7f928ca80 Mon Sep 17 00:00:00 2001 From: rdu Date: Thu, 23 Apr 2026 16:08:09 +0800 Subject: [PATCH 1/5] fix: resolve autoSave race condition in frontend template --- handlers.go | 78 ++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/handlers.go b/handlers.go index 7959b3f..7a408b9 100644 --- a/handlers.go +++ b/handlers.go @@ -626,6 +626,8 @@ func renderHTML(w http.ResponseWriter, noteID string, content string, r *http.Re const printableEl = document.getElementById("printable"); const toastEl = document.getElementById("toast"); + let saving = false; + function setStatus(text, state) { statusText.textContent = text; statusDot.className = 'status-dot ' + (state || 'ready'); @@ -649,43 +651,47 @@ func renderHTML(w http.ResponseWriter, noteID string, content string, r *http.Re // Auto-save function autoSave() { - if (textarea.value !== lastSaved) { - setStatus('Saving...', 'saving'); - - const saveUrl = currentNoteId ? appBase + 'noteid/' + currentNoteId : appBase; - fetch(saveUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ noteId: currentNoteId, content: textarea.value }) - }) - .then(function(response) { - if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText); - return response.json(); - }) - .then(function(data) { - if (data.success) { - lastSaved = textarea.value; - currentNoteId = data.noteId; - - var newPath = appBase + 'noteid/' + data.noteId; - if (window.location.pathname !== newPath && currentNoteId) { - window.history.replaceState({}, '', newPath); - document.getElementById('noteInfo').textContent = data.noteId; - } - - setStatus('Saved', 'saved'); - setTimeout(function() { - if (statusText.textContent === 'Saved') setStatus('Ready', 'ready'); - }, 2000); - } else { - setStatus('Error: ' + (data.error || 'Save failed'), 'error'); + if (saving || textarea.value === lastSaved) return; + saving = true; + setStatus('Saving...', 'saving'); + + const contentToSave = textarea.value; + const saveUrl = currentNoteId ? appBase + 'noteid/' + currentNoteId : appBase; + fetch(saveUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ noteId: currentNoteId, content: contentToSave }) + }) + .then(function(response) { + if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText); + return response.json(); + }) + .then(function(data) { + if (data.success) { + lastSaved = contentToSave; + currentNoteId = data.noteId; + + var newPath = appBase + 'noteid/' + data.noteId; + if (window.location.pathname !== newPath && currentNoteId) { + window.history.replaceState({}, '', newPath); + document.getElementById('noteInfo').textContent = data.noteId; } - }) - .catch(function(err) { - console.error('Save error:', err); - setStatus('Error: ' + (err.message || 'Network error'), 'error'); - }); - } + + setStatus('Saved', 'saved'); + setTimeout(function() { + if (statusText.textContent === 'Saved') setStatus('Ready', 'ready'); + }, 2000); + } else { + setStatus('Error: ' + (data.error || 'Save failed'), 'error'); + } + }) + .catch(function(err) { + console.error('Save error:', err); + setStatus('Error: ' + (err.message || 'Network error'), 'error'); + }) + .finally(function() { + saving = false; + }); } setInterval(autoSave, 1000); From 3b31c0f3403a0df6ea70971765d2bd5afa0a6f63 Mon Sep 17 00:00:00 2001 From: rdu Date: Thu, 23 Apr 2026 16:15:46 +0800 Subject: [PATCH 2/5] feat: align random note ID generation with v0.2.6 format --- handlers_test.go | 18 +++---- storage_test.go | 12 ++--- utils.go | 120 ++++++++++++++++++++++++++++++++++++++++++----- utils_test.go | 35 +++++--------- 4 files changed, 134 insertions(+), 51 deletions(-) diff --git a/handlers_test.go b/handlers_test.go index b1cfa87..979b369 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -67,10 +67,10 @@ func TestHandleGetEmpty(t *testing.T) { // TestHandleGetExisting tests GET request for existing note func TestHandleGetExisting(t *testing.T) { storage := NewMockStorage() - _ = storage.Write(context.Background(), "test123", "test content") + _ = storage.Write(context.Background(), "TEST23", "test content") handler := HandleGet(storage) - req := httptest.NewRequest("GET", "/?note=test123", nil) + req := httptest.NewRequest("GET", "/?note=TEST23", nil) rec := httptest.NewRecorder() handler(rec, req) @@ -131,7 +131,7 @@ func TestHandlePostExisting(t *testing.T) { handler := HandlePost(storage) payload := NoteRequest{ - NoteID: "test123", + NoteID: "TEST23", Content: "updated content", } body, _ := json.Marshal(payload) @@ -155,7 +155,7 @@ func TestHandlePostExisting(t *testing.T) { t.Errorf("Expected success=true") } - if content, _ := storage.Read(context.Background(), "test123"); content != "updated content" { + if content, _ := storage.Read(context.Background(), "TEST23"); content != "updated content" { t.Errorf("Expected content to be updated") } } @@ -194,12 +194,12 @@ func TestHandlePostInvalidID(t *testing.T) { // TestHandlePostDelete tests POST request with empty content (delete) func TestHandlePostDelete(t *testing.T) { storage := NewMockStorage() - _ = storage.Write(context.Background(), "test123", "original content") + _ = storage.Write(context.Background(), "TEST23", "original content") handler := HandlePost(storage) payload := NoteRequest{ - NoteID: "test123", + NoteID: "TEST23", Content: "", } body, _ := json.Marshal(payload) @@ -215,7 +215,7 @@ func TestHandlePostDelete(t *testing.T) { } // Verify note was deleted - if content, _ := storage.Read(context.Background(), "test123"); content != "" { + if content, _ := storage.Read(context.Background(), "TEST23"); content != "" { t.Errorf("Expected note to be deleted") } } @@ -263,14 +263,14 @@ func TestParseNoteRequestJSONPath(t *testing.T) { // TestParseNoteRequestFormAndRaw tests form parsing and raw body path fallback func TestParseNoteRequestFormAndRaw(t *testing.T) { - form := "text=hi¬eId=FORMID" + form := "text=hi¬eId=FRMID" req := httptest.NewRequest("POST", "/", bytes.NewBufferString(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") noteReq, _, err := parseNoteRequest(req, []byte(form), "127.0.0.1") if err != nil { t.Fatalf("unexpected error: %v", err) } - if noteReq.NoteID != "FORMID" || noteReq.Content != "hi" { + if noteReq.NoteID != "FRMID" || noteReq.Content != "hi" { t.Fatalf("unexpected form parse result: %#v", noteReq) } diff --git a/storage_test.go b/storage_test.go index 9fefbc9..c0b8743 100644 --- a/storage_test.go +++ b/storage_test.go @@ -13,7 +13,7 @@ func TestLocalStorageRead(t *testing.T) { // Write test file testContent := "test content" - filePath := filepath.Join(tmpDir, "test123") + filePath := filepath.Join(tmpDir, "TEST23") if err := os.WriteFile(filePath, []byte(testContent), 0644); err != nil { t.Fatalf("Failed to write test file: %v", err) } @@ -25,7 +25,7 @@ func TestLocalStorageRead(t *testing.T) { } // Test read - content, err := storage.Read(context.Background(), "test123") + content, err := storage.Read(context.Background(), "TEST23") if err != nil { t.Fatalf("Failed to read note: %v", err) } @@ -65,13 +65,13 @@ func TestLocalStorageWrite(t *testing.T) { testContent := "test content" // Test write - err = storage.Write(context.Background(), "test123", testContent) + err = storage.Write(context.Background(), "TEST23", testContent) if err != nil { t.Fatalf("Failed to write note: %v", err) } // Verify file exists - filePath := filepath.Join(tmpDir, "test123") + filePath := filepath.Join(tmpDir, "TEST23") if _, err := os.Stat(filePath); err != nil { t.Fatalf("Note file not created: %v", err) } @@ -92,13 +92,13 @@ func TestLocalStorageDelete(t *testing.T) { } // Create test file - filePath := filepath.Join(tmpDir, "test123") + filePath := filepath.Join(tmpDir, "TEST23") if err := os.WriteFile(filePath, []byte("content"), 0644); err != nil { t.Fatalf("Failed to write test file: %v", err) } // Test delete - err = storage.Delete(context.Background(), "test123") + err = storage.Delete(context.Background(), "TEST23") if err != nil { t.Fatalf("Failed to delete note: %v", err) } diff --git a/utils.go b/utils.go index 0a6d41f..b42ca57 100644 --- a/utils.go +++ b/utils.go @@ -1,32 +1,128 @@ package main import ( + "crypto/rand" "html" - "math/rand" "net" "net/http" "regexp" "strings" ) -// ValidateNoteID checks if a note ID is valid (alphanumeric only) +var WORD_LIST = []string{ + // 3-letter words + "ACE", "AGE", "AXE", "BAG", "BAR", "BAT", "BAY", "BED", "BEE", "BEG", + "BET", "BUD", "BUN", "BUS", "CAB", "CAN", "CAP", "CAR", "CAT", "CUB", + "CUP", "CUT", "DAM", "DAY", "DEN", "DEW", "DRY", "DUN", "EAR", "EEL", + "EGG", "ELK", "ELM", "EWE", "FAD", "FAR", "FAT", "FAX", "FED", "FEW", + "FLY", "FUN", "FUR", "GAP", "GAS", "GEL", "GEM", "GUN", "GUT", "GUY", + "GYM", "HAM", "HAT", "HAY", "HEN", "HEW", "HUB", "HUG", "HUM", "JAB", + "JAM", "JAR", "JAW", "JAY", "JET", "JUG", "KEG", "KEY", "LAB", "LAD", + "LAP", "LAW", "LAX", "LAY", "LEG", "LET", "LUG", "MAD", "MAP", "MAR", + "MAT", "MAY", "MUD", "MUG", "NAB", "NAG", "NAP", "NAY", "NET", "NEW", + "NUB", "NUN", "NUT", "PAD", "PAN", "PAR", "PAT", "PAW", "PAY", "PEA", + "PEG", "PEN", "PEP", "PET", "PEW", "PUN", "PUP", "PUT", "RAG", "RAM", + "RAN", "RAP", "RAT", "RAW", "RAY", "RED", "RUB", "RUG", "RUM", "RUN", + "RUT", "SAD", "SAP", "SAT", "SAW", "SAY", "SEA", "SET", "SEW", "SKY", + "SLY", "SPY", "SUB", "SUM", "SUN", "TAB", "TAD", "TAN", "TAP", "TAR", + "TAX", "TEA", "TEN", "TUB", "TUG", "URN", "VAN", "VAT", "VET", "VEX", + "WAD", "WAR", "WAX", "WAY", "WEB", "WED", "WET", "WRY", "YAK", "YAM", + "YAP", "YEA", "YEW", "ZAP", "ZEN", + // 4-letter words + "ABLE", "ARCH", "ARMY", "AUNT", "BACK", "BALL", "BAND", "BANK", "BARN", "BASE", + "BATH", "BEAR", "BEAT", "BECK", "BELL", "BELT", "BEST", "BLEW", "BLUE", "BLUR", + "BULK", "BURN", "BUSY", "CALM", "CAME", "CAMP", "CANE", "CARD", "CARE", "CASE", + "CASH", "CAST", "CAVE", "CLAM", "CLAP", "CLAW", "CLAY", "CLUE", "CLUB", "CREW", + "CURE", "CUTE", "DARK", "DATA", "DATE", "DAWN", "DAYS", "DEAD", "DEAF", "DEAL", + "DEAR", "DEBT", "DEED", "DEEP", "DEER", "DELL", "DENY", "DESK", "DRAW", "DRUM", + "DUAL", "DUNE", "DUSK", "DUST", "DUTY", "EACH", "EARN", "EASE", "EAST", "EDGE", + "ELSE", "EVEN", "EVER", "EXAM", "FACE", "FACT", "FALL", "FAME", "FARM", "FAST", + "FATE", "FEEL", "FEET", "FELL", "FELT", "FERN", "FLAT", "FLAW", "FLAX", "FLEX", + "FLEW", "FUND", "FUSE", "GALE", "GAME", "GANG", "GAZE", "GEAR", "GENE", "GLAD", + "GLUE", "GRAB", "GRAM", "GRAY", "GREW", "GULF", "GUST", "HALF", "HALL", "HALT", + "HAND", "HANG", "HARD", "HARM", "HARP", "HAVE", "HAWK", "HAZE", "HEAD", "HEAL", + "HEAP", "HEAT", "HEEL", "HELM", "HELP", "HERB", "HERE", "HULL", "HUNT", "HURT", + "HUSK", "JADE", "JAZZ", "JUMP", "JUST", "KEEN", "KEEP", "KNEW", "LACE", "LACK", + "LAKE", "LAMP", "LAND", "LANE", "LARK", "LASH", "LAST", "LATE", "LAVA", "LAWN", + "LEAD", "LEAF", "LEAK", "LEAN", "LEAP", "LEND", "LENS", "LUCK", "LURE", "LUSH", + "MACE", "MALL", "MALT", "MARE", "MARK", "MART", "MAST", "MAZE", "MEAL", "MEAN", + "MEAT", "MEET", "MELT", "MEND", "MENU", "MESH", "MUCH", "MULE", "MURK", "MUSE", + "NAME", "NECK", "NEED", "NEST", "NEXT", "NULL", "PACE", "PACK", "PAGE", "PALE", + "PALM", "PARK", "PART", "PAST", "PATH", "PEAK", "PEAR", "PEAT", "PEEL", "PEER", + "PLAN", "PLUM", "PLUS", "PREY", "PULL", "PUMP", "PURE", "PUSH", "RACE", "RACK", + "RAMP", "RANK", "RASP", "READ", "REAL", "REED", "REEF", "REEL", "RELY", "REST", + "RUBY", "RULE", "RUSH", "RUST", "SAFE", "SAGE", "SALT", "SAND", "SCAR", "SEAL", + "SEAM", "SEEN", "SELF", "SELL", "SHED", "SHUT", "SLAB", "SLAP", "SLEW", "SLUM", + "SNAP", "SPAN", "SPAR", "SPUR", "STAR", "STAY", "STEM", "STEP", "STUB", "SUCH", + "SULK", "SURF", "SWAN", "SWAP", "TALE", "TALL", "TANK", "TARP", "TASK", "TEAL", + "TEAR", "TELL", "TERM", "TEXT", "THAN", "THAT", "THEM", "THEN", "THEY", "TRAP", + "TRAY", "TREE", "TREK", "TRUE", "TUBE", "TUNE", "TURF", "TURN", "TUSK", "TYPE", + "VALE", "VANE", "VAST", "VENT", "VEST", "WADE", "WAKE", "WALK", "WALL", "WARD", + "WARM", "WARP", "WART", "WAVE", "WEAK", "WELD", "WELL", "WEST", "WHEY", "WREN", + "YANK", "YEAR", "YELL", "ZEAL", "ZEST", + // 5-letter words + "BEACH", "BEARD", "BEAST", "BLACK", "BLADE", "BLAME", "BLAND", "BLANK", "BLAST", "BLAZE", + "BLEAK", "BLEED", "BLEND", "BLESS", "BLUFF", "BLUNT", "BLUSH", "BRAND", "BRAVE", "BRAWL", + "BRAWN", "BREAD", "BREAK", "BREAM", "BREED", "BRUSH", "BRUTE", "BULGE", "BUNCH", "BURST", + "CAMEL", "CANDY", "CARRY", "CEDAR", "CHALK", "CHAMP", "CHANT", "CHASE", "CHEEK", "CHEER", + "CHESS", "CHEST", "CHURN", "CLAMP", "CLANG", "CLANK", "CLEAT", "CLERK", "CLUNG", "CRAFT", + "CRANE", "CREAK", "CREAM", "CREEK", "CREEP", "CREPT", "CREST", "CRUMB", "CRUSH", "CRUST", + "DELTA", "DENSE", "DEPTH", "DRAFT", "DRAMA", "DRAPE", "DRANK", "DRAWL", "DREAD", "DREAM", + "DRESS", "DWELL", "DWELT", "EAGLE", "EARLY", "EARTH", "ERASE", "EXACT", "EXTRA", "EXULT", + "FABLE", "FATAL", "FAULT", "FEAST", "FENCE", "FERRY", "FETCH", "FEVER", "FEWER", "FLAME", + "FLANK", "FLASK", "FLASH", "FLEET", "FLESH", "FLUNG", "FLUTE", "FLYER", "FREAK", "FRESH", + "FUDGE", "FULLY", "GAUNT", "GAVEL", "GLAND", "GLARE", "GLASS", "GLEAM", "GLEAN", "GNASH", + "GRAFT", "GRAND", "GRANT", "GRASP", "GRASS", "GRAZE", "GREET", "GRUFF", "GRUEL", "GRUNT", + "GUARD", "GUESS", "GUEST", "GULCH", "HARSH", "HASTE", "HAVEN", "HAZEL", "HEARD", "HEART", + "HEAVE", "HEAVY", "HEDGE", "HENCE", "HUMAN", "HYDRA", "KAYAK", "KNACK", "KNEEL", "LANCE", + "LARGE", "LASER", "LATCH", "LATER", "LAUGH", "LAYER", "LEARN", "LEASH", "LEDGE", "LUCKY", + "LUNAR", "LUNGE", "LUSTY", "MAKER", "MAPLE", "MARCH", "MARSH", "MATCH", "MEDAL", "MERRY", + "METAL", "METER", "NAMED", "NERVE", "NEVER", "PATCH", "PAUSE", "PEACE", "PEACH", "PEDAL", + "PERCH", "PHASE", "PLANK", "PLANT", "PLATE", "PLAZA", "PLEAD", "PLEAT", "PLUCK", "PLUMB", + "PLUME", "PLUNK", "PLUSH", "PRANK", "PRESS", "PRUNE", "PSALM", "PUMPS", "PURSE", "PYGMY", + "QUART", "QUASH", "QUEEN", "QUELL", "QUERY", "QUEST", "QUEUE", "RANCH", "REACH", "REACT", + "REALM", "REBUT", "RELAX", "REPAY", "REPEL", "RESET", "REUSE", "REVEL", "RUGBY", "RULED", + "RUPEE", "RURAL", "RUSTY", "SADLY", "SAUCE", "SCALD", "SCALE", "SCALP", "SCALY", "SCAMP", + "SCANT", "SCARE", "SCARY", "SCENE", "SCENT", "SCRUB", "SEEDY", "SERVE", "SETUP", "SEVEN", + "SHADE", "SHADY", "SHAKE", "SHALL", "SHALE", "SHAME", "SHAPE", "SHARE", "SHARK", "SHARP", + "SHAVE", "SHAWL", "SHEAR", "SHEEN", "SHELF", "SHELL", "SHRUG", "SHUNT", "SKATE", "SKULL", + "SKUNK", "SLACK", "SLANT", "SLAVE", "SLEEK", "SLEET", "SLEPT", "SLURP", "SMACK", "SMALL", + "SMASH", "SMELL", "SMELT", "SNACK", "SNARE", "SNARL", "SNEAK", "SNEER", "SNUCK", "SNUFF", + "SPARE", "SPARK", "SPASM", "SPAWN", "SPEAK", "SPEAR", "SPELL", "SPELT", "SPEND", "SPENT", + "SPUNK", "SQUAD", "SQUAT", "STACK", "STAFF", "STAGE", "STALE", "STALL", "STAMP", "STAND", + "STARE", "STARK", "START", "STEAL", "STEEL", "STEEP", "STEER", "STERN", "STUNG", "STUNK", + "STUNT", "STYLE", "SUPER", "SURGE", "SWAMP", "SWATH", "SWEAR", "SWEAT", "SWEEP", "SWEET", + "SWELL", "SWUNG", "TAMED", "TEACH", "TENSE", "TENTH", "TERMS", "THANK", "THEME", "THERE", + "THESE", "TRACE", "TRACK", "TRADE", "TRAMP", "TRASH", "TRAWL", "TREAD", "TREAT", "TREND", + "TRUCE", "TRUCK", "TRULY", "TRUMP", "TRUNK", "TRUSS", "TRUST", "ULCER", "ULTRA", "UNDER", + "UNDUE", "UNSET", "UPSET", "URBAN", "USAGE", "USHER", "USURP", "VAGUE", "VALUE", "VAULT", + "VEGAN", "VENUE", "VERSE", "VERGE", "VERVE", "VEXED", "WAGER", "WAKEN", "WATER", "WEARY", + "WEDGE", "WEEDY", "WELCH", "WHALE", "WHACK", "WHEAT", "WHEEL", "WHELP", "WHERE", "ZAPPY", "ZEBRA", +} + +// ValidateNoteID checks if a note ID is valid func ValidateNoteID(noteID string) bool { - if noteID == "" { + if noteID == "" || len(noteID) > 32 { return false } - matched, _ := regexp.MatchString("^[a-zA-Z0-9]+$", noteID) + matched, _ := regexp.MatchString("^[A-HJ-NP-Z2-9]{3,32}$", noteID) return matched } -// GenerateNoteID creates a random 5-character alphanumeric note ID +// GenerateNoteID creates a random note ID func GenerateNoteID() string { - const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" - const length = 5 - b := make([]byte, length) - for i := range b { - b[i] = charset[rand.Intn(len(charset))] - } - return string(b) + wordBytes := make([]byte, 1) + _, _ = rand.Read(wordBytes) + word := WORD_LIST[int(wordBytes[0])%len(WORD_LIST)] + + digitCharset := "23456789" + digitBytes := make([]byte, 2) + _, _ = rand.Read(digitBytes) + + d1 := digitCharset[int(digitBytes[0])%len(digitCharset)] + d2 := digitCharset[int(digitBytes[1])%len(digitCharset)] + + return word + string(d1) + string(d2) } // EscapeHTML escapes HTML special characters diff --git a/utils_test.go b/utils_test.go index 5c040ad..00069b2 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,7 +1,6 @@ package main import ( - "regexp" "testing" ) @@ -11,17 +10,20 @@ func TestValidateNoteID(t *testing.T) { id string valid bool }{ - {"abc123", true}, - {"ABC", true}, - {"123", true}, - {"a", true}, - {"", false}, + {"ACE23", true}, + {"BLAST47", true}, + {"ZEBRA99", true}, + {"123", false}, // Contains 1 + {"a", false}, // Lowercase + {"", false}, // Empty {"abc-def", false}, {"abc_def", false}, {"abc@def", false}, {"abc def", false}, {"abc.def", false}, {"../etc", false}, + {"ABCDEFGHJKLMNPQRSTUVWXYZ23456789", true}, // 32 chars + {"ABCDEFGHJKLMNPQRSTUVWXYZ23456789A", false}, // 33 chars } for _, test := range tests { @@ -39,14 +41,9 @@ func TestGenerateNoteID(t *testing.T) { for i := 0; i < tests; i++ { id := GenerateNoteID() - // Check length - if len(id) != 5 { - t.Errorf("Generated ID has length %d, expected 5", len(id)) - } - - // Check alphanumeric - if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(id) { - t.Errorf("Generated ID %s is not alphanumeric", id) + // Check valid length + if len(id) < 5 || len(id) > 7 { + t.Errorf("Generated ID has length %d, expected 5-7", len(id)) } // Check it's valid @@ -54,16 +51,6 @@ func TestGenerateNoteID(t *testing.T) { t.Errorf("Generated ID %s is not valid", id) } } - - // Check uniqueness (probabilistic) - ids := make(map[string]bool) - for i := 0; i < 100; i++ { - id := GenerateNoteID() - if ids[id] { - t.Logf("Warning: Generated duplicate ID %s (may be rare)", id) - } - ids[id] = true - } } // TestEscapeHTML tests HTML escaping From f35741df1a32f2b9a9a39034004435df4d3378b8 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 24 Apr 2026 03:42:33 +0000 Subject: [PATCH 3/5] fix: preserve path noteID for curl raw body posts with form content-type --- handlers.go | 1 + handlers_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/handlers.go b/handlers.go index 7a408b9..09818fd 100644 --- a/handlers.go +++ b/handlers.go @@ -194,6 +194,7 @@ func parseNoteRequest(r *http.Request, bodyBytes []byte, clientIP string) (NoteR return req, contentType, nil } req.Content = string(bodyBytes) + req.NoteID = extractPathNoteID(r) log.Printf("[INFO] Received %d bytes from raw form body from %s", len(bodyBytes), clientIP) return req, contentType, nil } diff --git a/handlers_test.go b/handlers_test.go index 979b369..9466d5a 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -284,4 +284,17 @@ func TestParseNoteRequestFormAndRaw(t *testing.T) { if noteReq.NoteID != "RAWID" || noteReq.Content != "raw body" { t.Fatalf("unexpected raw parse result: %#v", noteReq) } + + // curl --data-binary sets Content-Type: application/x-www-form-urlencoded but body is plain text + // NoteID must be taken from the path, not ignored + req = httptest.NewRequest("POST", "/noteid/ABCDE", bytes.NewBufferString("hello content")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("User-Agent", "curl/8.5.0") + noteReq, _, err = parseNoteRequest(req, []byte("hello content"), "127.0.0.1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if noteReq.NoteID != "ABCDE" || noteReq.Content != "hello content" { + t.Fatalf("curl raw body via form content-type lost path noteID: %#v", noteReq) + } } From a088db102bb2786e56d5d744b92ad36465232900 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 24 Apr 2026 03:48:19 +0000 Subject: [PATCH 4/5] ci: add feature/golang-v0.2.6 branch to test workflow triggers --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4dac683..7a7a769 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Tests on: push: - branches: [main, dev] + branches: [main, dev, feature/golang-v0.2.6] pull_request: - branches: [main, dev] + branches: [main, dev, feature/golang-v0.2.6] workflow_dispatch: env: From 8b4d4b6c633f28f5f47a1ecf8834cd83d08d0ca9 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 24 Apr 2026 03:53:59 +0000 Subject: [PATCH 5/5] ci: update workflows to trigger on golang branch --- .github/workflows/deploy-lambda.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-lambda.yml b/.github/workflows/deploy-lambda.yml index 9910fea..18cc525 100644 --- a/.github/workflows/deploy-lambda.yml +++ b/.github/workflows/deploy-lambda.yml @@ -2,7 +2,7 @@ name: Deploy to AWS Lambda (Optional) on: push: - tags: ['v*'] + branches: [golang] workflow_dispatch: inputs: environment: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a7a769..35f0f54 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Tests on: push: - branches: [main, dev, feature/golang-v0.2.6] + branches: [golang] pull_request: - branches: [main, dev, feature/golang-v0.2.6] + branches: [golang] workflow_dispatch: env: