Repository navigation
v1.6.0 polish: middleware #888
Description
Consolidated on 2026-10-02 from the per-PR 'Follow-ups from #N' bundles, by maintainer decision. From now on, review nits are fixed inside the PR before it merges, so no new bundles are filed. Each line names the bundle and item it came from; the bundle's thread keeps the full context. Everything here stays in v1.6.0.
-
middleware/otel/carrier.go: headerCarrier.Get clones traceparent too, a per-request alloc with no benefit (W3C propagator decodes it into fixed arrays); skip it or document it as deliberate, widen the doc's reason (sampled spans keep TraceState past the request), and make Keys() use one buffer. (from Follow-ups from #723: otel traceparent clone cost, SSE OnConnect and session c.Set views, stale #714/#718 scope, ENOMEM claim wording, deliberate-red record #740 1) (Done: otel: Keys builds its names from one allocation; carrier docs on retained substrings #1040 [main])
-
In middleware/websocket/engineread_latespill_test.go (TestChanReaderSpillPublishedAfterDrain, case read-waiting-in-window) replace time.Sleep(50ms) with waitLockWaiter(t, '(*chanReader).promoteSpill'), make waitLockWaiter release r.spillMu before t.Fatalf (onFail callback), update the comment, and confirm mutant N3 still fails the case. (from Follow-ups from #730: a sleep used as synchronisation in read-waiting-in-window, start-helper comments stale after #747 #782 1) (Done: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046 [main])
-
Since fix(server): a Start that never serves releases the caller's listener, the CPU monitor and the settle re-opener (#737) #747 a failed Start closes the caller's listener and the CPU monitor, but comments still say it closes neither: middleware/websocket/engine_linux_test.go:80-83, middleware/websocket/engine_start_test.go:56-57, middleware/static/start_helper_linux_test.go:49-50, middleware/static/retained_key_linux_test.go:105; reword and decide whether each explicit release stays as a guard. (from Follow-ups from #730: a sleep used as synchronisation in read-waiting-in-window, start-helper comments stale after #747 #782 2) (Done: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046, tests: start helpers retry ENOMEM and fail rather than skip; probe retry logs its rounds (checklist #888 items 3, 31, #890 item 6) #1047 [main])
-
test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749's body m607 row says 'io_uring subtest FAILED 10/10' without naming the test; only TestBackpressurePauseDoesNotCancelInflightSend catches m607 and the inbound oracle's io_uring: a linked recv never starts because its chained SEND blocks on a detached WebSocket peer's closed window #607 checks cannot fire at CI size. Correct the wording, and give the inbound checks a mutant that exercises them. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 1) (PARTIAL: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046; open: PARTIAL only on the wording: the test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 body table row is unchanged and the 2026-10-20 "Corr)
-
The wsoQuiet comment (middleware/websocket/backpressure_oracle_linux_test.go:157) says a io_uring: a linked recv never starts because its chained SEND blocks on a detached WebSocket peer's closed window #607 stretch began up to 1.6 s after the flood; the head's own records show ~2.8 s and x86 clears the 5 s limit by ~1 s. Correct the figure, state the margin, consider a longer quiet. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 2) (Done: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046 [main])
-
The body's healthy-margin and watch-lag claims pool runs that did not measure the lag (128 of 218 runs carry watchMaxLag; maxima from pre-quiet candidate B-fix2), and the watch and linked-recv assertion never ran with multi-worker io_uring; correct the wording and add a 128 MiB memlock or cluster run. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 3) (open: MEASUREMENT test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749)
-
The inbound oracle counts a server-side read ECONNRESET (serverRST, inbound_sequence_linux_test.go:192) and never judges it; assert it (excused on a give-up like the other errors), and narrow the body's 'Every other error is judged'. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 4) (PARTIAL: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046; open: Assertion half DONE (websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046). Wording half not done: the test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 body sentence "Every other er)
-
test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 body's 'Not in this PR' bullet has ci: set up CodeRabbit, Codecov, CodSpeed (celeris#690) #699 backwards: ./middleware/websocket is already excluded from test-coverage.yml by merged ci: set up CodeRabbit, Codecov, CodSpeed (celeris#690) #699; the follow-up is putting it back. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 5) (Done: test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 [main])
-
test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 body's range-diff sentence mixes old and new commit ids (13ca047/3421840 are the rebased commits, not the ones on 698bed6). (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 6) (Done: test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 [main])
-
Two test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 body phrasings go beyond the data: '4.5 s' should be 'as low as 4.5 s', and main's oracle dropped connections, not bursts, from its frame count. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 7) (Done: test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 [main])
-
The oracle watch's CPU-starvation guard (backpressure_oracle_linux_test.go:1142-1206) measures only the wait for a tick, not a slow pass, and samples now after srvViewOf, so starvation can stretch an episode to wsoRecvStall and fail the test with no engine fault; reset a connection's episode when the gap since its previous sample exceeds wsoWatchEvery+wsoWatchGap. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 8) (Done: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046 [main])
-
test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 body line 'RecvLinkedArms == 0, io_uring, both tests' overstates coverage: at CI size the inbound test's linked-recv assertion recorded 0 arms under m607; only PauseCancel's has fired. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 11) (Done: test(websocket): the backpressure oracles wait on progress and read while they wait, judge every give-up with both ends' timeline, and fail a connection the engine stops reading (celeris#633, celeris#623, celeris#611, celeris#607 class) #749 [main])
-
pause_cancel_linux_test.go:137-140 says the recv witnesses 'are direct atomics', but RecvLinkedArms goes through io_uring's worker-local linkArmBatch flushed once per iteration, so last-iteration arms can be lost (false negative only); fix the comment or flush before reading. (from Follow-ups from #749: the oracle watch's starvation guard, the epoll 1.2-3.9 s read stretches, the inbound #607 checks that cannot fire at CI size, and body wording #788 12) (Done: websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046 [main])
-
middleware/sse TestSlowClientBlockPolicy is a timing knife-edge (gate's 30 ms timer starts before the handler's time.Now, floor gateDelay-5ms; failed once in fix(iouring): require Linux 5.19; refuse io_uring on a kernel that rejects the IORING_ASYNC_CANCEL flags (celeris#682) #792 CI run 36365460899); start the handler's clock no later than the gate timer or measure from the gate's own start. Not filed elsewhere. (from Follow-ups from #792: io_uring 5.19 floor — backport probe covers 1 of 4 cancel forms, floor test keyed on version, no CI seam for adaptive/Server, stale comments, SSE flake, docs pages #796 8) (Done: sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 [main])
-
Correct the stale comment at middleware/sse/sse.go:543-552 claiming pipelined bytes on a live SSE conn are parsed as a new request on the same cached stream/Context (predates fix(router, conn, sse): answer HEAD with the GET route and OPTIONS with the Allow list; a streamed HEAD sends no body (celeris#421, celeris#833) #841). (from Follow-ups from #841: HEAD now reaches #837 and #835, HEAD on Detach-streaming GET routes, the per-request isHEAD read on live streams, FullPath sentinels, OPTIONS opt-out #847 3c) (Done: sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 [main])
-
Investigate in a browser whether Scalar 1.72.4's default page sends 'Try it' requests through proxy.scalar.com (DEFAULT_PROXY_URL, proxyUrl default for 'web') and loads fonts.scalar.com (withDefaultFonts=true). If it proxies by default it is the same class as swagger: Swagger UI's online validator badge sends every viewer's spec URL to validator.swagger.io (validatorUrl unset) #849 (viewer data to a third party by default), and the middleware should set proxyUrl/withDefaultFonts defaults or document them. (from Follow-ups from #851: Scalar's AssetsPath file name, re-compressing the embedded bundle behind compress, Scalar's default third-party origins, a relative default spec URL #861 3) (PARTIAL: fix(swagger): serve Swagger UI from embedded assets by default; the CDN is opt-in, pinned and integrity-checked (celeris#425) #851, fix(swagger): no validator badge by default, serve the OAuth2 redirect page, a relative spec URL and redirect, Scalar's published file name (celeris#849, celeris#850, celeris#882, celeris#883) #915; open: NUMBERING Own run: open Scalar 1.72.4 default page in a browser (devtools network) and see)
-
basicauth (fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 at a519143), fix both in fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 before it merges (DONE in fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894, 2c81073: the comment in 8ee8257, the release-note line in the PR body and the merge commit): (a) middleware/basicauth/pbkdf2.go:19-20 says VerifyPassword "dispatches on" pbkdf2Tag; it no longer does (
func VerifyPasswordat :125 isreturn verifyPBKDF2(hash, password); the tag is checked only in parsePBKDF2, isPBKDF2Hash/allPBKDF2 and pickDummyHash). Reword. (b) fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894's release-note bullet should add that a store wired explicitly withHashedUsersFunc: VerifyPasswordthat still holds bare SHA-256 digests now passes New and answers 401 for those users on every request (TestHashedUsersBareSHA256EntryRejected826), while the auto-wired path (config.go:136) panics at New; base doc.go:55-58 recommended exactly that explicit wiring. It fails closed. (from fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 review r1: correctness NITs 2-3) -
csrf and session tests (fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 at a519143) shadow the newly imported package
storewith a local variable:store := NewMemoryStorage(store.MemoryKVConfig{...})at middleware/csrf/csrf_test.go:1086, 1112, 1130 and 14 more sites, andstore := NewMemoryStore(store....)at middleware/session/session_test.go:314, 2086, 2129 and one more. It compiles, but for the rest of each function the package cannot be reached. Rename the local (e.g.kv). Test-only: fix in fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 before it merges. (from fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 review r1: NIT from both reviewers) DONE in fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 (2c81073, from 8ee8257): the two test files import the package askvstore, so no localstoreshadows it. -
middleware/idempotency/store.go:7-9:
type MemoryStoreConfig = store.MemoryKVConfigsays it is "provided for symmetry with other middleware packages", but fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 removes those siblings (csrf.MemoryStorageConfig, session.MemoryStoreConfig). It is now the only package-local alias of store.MemoryKVConfig, the pattern Remove every deprecated public API before the public v1.6.0 release #830 removed, and it freezes at v1.6.0. After fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894, csrf/session NewMemory* take...store.MemoryKVConfigand return*store.MemoryKV, while idempotency.NewMemoryStore (:14) takes...MemoryStoreConfigand returns the KVStore interface. (cache.MemoryStoreConfig is a separate struct.) Decide before v1.6.0: remove it (breaking; label it) or keep it and fix the comment. Outside fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894: it carries no deprecation notice. (from fix(api): remove every deprecated public API and basicauth.HashPassword before the public v1.6.0 (celeris#830, celeris#826) #894 review r1: API/perf/security MINOR 3, correctness NIT 5) (done in swagger: Config.Options godoc says Swagger UI ignores it; idempotency.MemoryStoreConfig comment #1042 [main]; maintainer to confirm, box left open) -
middleware/swagger/config.go:132-134: the
Config.Optionsgodoc says that for Swagger UI the options "are passed to SwaggerUIBundle()".buildSwaggerUIPage(swagger.go:248-290 at 9e54e3b) never readscfg.Options; only the Scalar and ReDoc builders do, and base e2508c7 behaves the same. Either passOptionsinto theSwaggerUIBundle({...})call (JSON-marshalled as ReDoc's are, without overriding url/dom_id/presets), or correct the godoc. docs#85 now says that Swagger UI ignoresOptions; update it if the behaviour changes. (from docs#85 CodeRabbit thread on middleware-content.md:654) (Done: swagger: Config.Options godoc says Swagger UI ignores it; idempotency.MemoryStoreConfig comment #1042 [main])
Added 2026-10-03 (#923's round-1 reviews, lane F3T):
- middleware/cache (fix(cache): the coalesced fill stores the response and releases its followers before any client is written, so a slow leader's client no longer holds them (celeris#913) #923 at 87ecd45):
Store.Setnow runs inside the coalesced call and before every MISS write (captureAndStore, cache.go:102-107 and :210-212;executeAndStore, :139-148). With a slow store, followers wait forSet, and on epoll and adaptive so does every event loop holding a sync follower. The leader's own MISS and everySingleflight: falseMISS are written only afterSetreturns. Probe (Set sleeps 300 ms, HTTP/1.1, no fix(h2): bound the response bytes a connection holds for the peer's window; over the budget, handlers run on the pool and send as the windows open (celeris#893) #906): std leader 51 -> 368 ms, followers 40 -> 357 ms, sf=false MISS 51-60 -> 352-362 ms; epoll/pingbystanders 6/8 undelayed on main, 0/8 with fix(cache): the coalesced fill stores the response and releases its followers before any client is written, so a slow leader's client no longer holds them (celeris#913) #923. No client can trigger it and the in-memory store'sSettakes microseconds. Option: hand followers the encoded result beforeSet, but keep the key in flight untilSetreturns, which keeps "no window in which the key is neither in flight nor stored" without holding followers or loops forSet. Evidence:lanes-20261003/F3T/review923-perfsec/probes/zz_probe_r923_slowset_linux_test.go,logs/ctr/06-s-main.logvs07-s-fix.log. (from fix(cache): the coalesced fill stores the response and releases its followers before any client is written, so a slow leader's client no longer holds them (celeris#913) #923 review r1, perf/security MINOR 1; a behaviour change, so not done in the PR) (Done: fix(cache): the coalesced fill stores the response and releases its followers before any client is written, so a slow leader's client no longer holds them (celeris#913) #923, fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940, fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main])
Added 2026-10-03 (#920's round-2 verification, lane MW; the doc parts were fixed in #920 at 043073d):
- middleware/sse (fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920 at 043073d): no test runs a queued-mode
Sendinto a bareNewRingBufferstore end to end.TestSentEventSurvivesSendersNextRequestwraps the ring in queued mode (send_event_kept_732_linux_test.go:55-56), soappendQueued->ringStore.appendOwned, which keeps the event without copying it, is never run end to end. It is correct today (mutant MUTQ,sendQueued'sownEventremoved, is killed on all 6 queued native arms through the wire check), andappendOwned's doc now names the precondition. Add a unit test that sends in queued mode to a bare ring and checks the replayed entry after the caller's bytes change. Evidence:lanes-20261003/MW/verify-r2/correctness/logs/mutq.log. (from fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920 verify r2, correctness NIT 1) (Done: sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 [main]) - middleware/sse (fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920 at 043073d): a queued
Sendthat is dropped still pays the copy:sendQueuedrunsownEvent(sse.go:136) before the non-blocking enqueue, so with a full queue and the default drop policy (OnSlowClientnil) every droppedSendmakes one allocation the size of the event, where main made none (probeTestVerifyQueuedDropPathAllocs: base 0.0, head 1.0 allocs/op, 201/201 dropped). The cost falls on the overload path. Option: skip the copy when the queue is full andonSlowClientis nil (benign race: at worst it drops an event that would just have fit); the copy is still needed whenOnSlowClientis set, since the callback can keepe. TheSenddoc and fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920's cost row now state it. Evidence:lanes-20261003/MW/verify-r2/perf-security/probe/verify_perf_r2_test.go,logs/bench-verify.log(GATE lines). (from fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920 verify r2, perf/security NIT 2; a behaviour change, so not done in the PR) (Done: sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 [main]) - middleware/sse (fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920 at 043073d): in queued mode a user store that wraps
NewRingBuffer(a metrics or logging decorator) copies each event twice:appendQueued(sse.go:210-214) skips the ring's copy only for a*ringStore, and the wrapper'sAppendforwards toringStore.Append, which copies again. 235 -> 435 allocs and 34127 -> 50155 B per 100 events, against 235 -> 335 for the plain ring (BenchmarkVerifySendBurstQueuedWrapRing). Nothing leaks; theNewRingBufferandappendQueueddocs now say so. Option: let the ring recognise an event that is already owned. Evidence:lanes-20261003/MW/verify-r2/perf-security/logs/bench-verify.summary.txt. (from fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920 verify r2, perf/security NIT 3) (open: CODE-OTHER fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920)
Added 2026-10-04 (MW2's round-1 reviews of #939 and #940; behaviour or performance changes, so not done in the PRs):
- middleware/singleflight (fix(singleflight): a conditional request is not coalesced, Accept-Encoding is in the default key, and a waiter whose request differs in a header the leader's response varies on runs its own handler (celeris#912) #939 at c114334):
answersItsOwnHeaders(singleflight.go:225) passes every conditional request through, so identical revalidations (a CDN or browsers sending the sameIf-None-Match) are no longer coalesced either. Putting the validators (If-None-Match,If-Modified-Since,If-Match,If-Unmodified-Since) in the default key instead would keep those coalesced and still answer each request's own validator. Evidence:lanes-20261004/MW2/review-correctness-r1/probes/zz_review_vary_912_test.go. (from fix(singleflight): a conditional request is not coalesced, Accept-Encoding is in the default key, and a waiter whose request differs in a header the leader's response varies on runs its own handler (celeris#912) #939 review r1, correctness NIT) (Done: fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main]) - middleware/singleflight (fix(singleflight): a conditional request is not coalesced, Accept-Encoding is in the default key, and a waiter whose request differs in a header the leader's response varies on runs its own handler (celeris#912) #939): the default key (config.go
defaultKeyFunc) holds the rawAccept-Encodingvalue, so equivalent values (gzip, deflate, brvsgzip,deflate,br, or another order) do not coalesce, which weakens the stampede guard across mixed clients. Normalising it to the encodingcompresswould choose keeps the separation and coalesces more. (from fix(singleflight): a conditional request is not coalesced, Accept-Encoding is in the default key, and a waiter whose request differs in a header the leader's response varies on runs its own handler (celeris#912) #939 review r1, perf/API NIT) (done in fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main]; maintainer to confirm, box left open) - middleware/internal/sf (fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940):
wait(group.go) starts a goroutine and a channel for every follower whose context has aDonechannel (std, HTTP/2, the timeout middleware), to select on the WaitGroup. A done channel on theCall, closed by the leader when it publishes, would let the follower select on it directly; it needs a channel per call (or one made lazily under the group mutex when such a follower joins), so measure the leader path. fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 removes the error-path allocations of the same review item (3 -> 0 per leader error without followers). Evidence:lanes-20261004/MW2/review-perf-r1/logs/rv1-940-sf-err-allocs.log. (from fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 review r1, perf MINOR, second half) (Done: fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main])
Added 2026-10-04 (MW2's round-2 reviews of #939 and #940. These are behaviour changes, so they are not done in the PRs. The trivial items were fixed in the PRs: compress's duplicate Vary and the #940 test windows. The split-Cookie defect is #944):
- middleware/cache and middleware/singleflight (fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 at 714dfcc, fix(singleflight): a conditional request is not coalesced, Accept-Encoding is in the default key, and a waiter whose request differs in a header the leader's response varies on runs its own handler (celeris#912) #939 at efd6c64): a leader's error that comes from its own request is handed to every follower. For example, the leader's client goes away, its context is cancelled and the handler returns
context.Canceled. The cache follower returnserrunchanged (cache.go:131-132), and the singleflight waiter returnsentry.err(singleflight.go:149, set at:195). The source issf.Group.Do, which handshandoff.Error(err)to followers (internal/sf/group.go:167). A client can therefore start a request for a hot key, abort it, and fail every concurrent follower of that call on an engine that cancels the request context (std, HTTP/2). Option: treatcontext.CanceledandDeadlineExceededfrom the leader's own context likeErrLeaderPanicked, so each follower runs its own handler. (from fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 review r2, API/perf/security MINOR 2; code reading, no probe) (Done: fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main]) - middleware/cache (fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 at 714dfcc):
Cache-Control: max-age=0andno-cacheresponses are stored for the fullConfig.TTL.parseMaxAgereturns 0, thed > 0guard keeps the TTL (cache.go:214-215), andno-cacheis never checked. TheIgnoreCacheControlgodoc and docs#93 say "max-age=N caps the TTL", which is false for N=0. Probe: for bothmax-age=0andno-cache, the second request gets v1/HIT with 1 handler run; theno-storecontrol gets v2/MISS. main a43aa1d behaves the same. Don't store either (or storeno-cacheonly as a response that must be revalidated). Evidence:lanes-20261004/MW2/review-r2-apisec/h940/middleware/cache/zz_review_r2_maxage0_test.go,maxage0-940.log,maxage0-base.log. (from fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 review r2, API/perf/security MINOR 3) (Done: fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main]) - middleware/cache (fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 at 714dfcc): a
Store.Setthat never returns still wedges the leader request, though no longer its followers.thenruns insideDobeforewriteMiss(cache.go:117-123), andSetgets the leader's request context, which never ends on native HTTP/1. On epoll and io_uring it also holds the event loop that a sync leader runs on. After each TTL, a new leader for the key can hang the same way. Option: a cache-level timeout onStore.Set, or write the leader's response before storing it. (from fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 review r2, API/perf/security NIT; code reading) (open: DECISION fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007) - middleware/timeout (cache: a coalesced fill whose handler panics never releases its key, so every later request for it waits forever (on epoll, io_uring and adaptive, with its event loop: one panic wedges the server) #921 family): a handler that panics skips
c.SetContext(origCtx)andlazy.release()(timeout.go:60-61). A deadline context thatDone()promoted is then cancelled only when its deadline fires. This is bounded, unlike the circuitbreaker, overload and ratelimit cases fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 fixed. A deferred restore and release closes it. (from fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 review r2, correctness NIT, first half) (Done: timeout restores the context when the handler panics; an unrouted OPTIONS request walks the router once #1041 [main]) - middleware/cache with internal/sf (fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 at 714dfcc): after a call is replaced on expiry (
sf.Group.Domarks the old callreplaced), the replaced call's hungSetcan still return after the newer call'sSetand overwrite the fresher entry. This is inherent to aSetthat hangs and then returns, and it happens withDisableSingleflighttoo. Decide whether to document it or have the store write compare a generation. (from fix(cache, idempotency, swagger)!: a coalesced fill that panics releases its key, followers no longer wait for the store's Set, and true-by-default options are inverted so a Config literal keeps them (celeris#921, celeris#922) #940 review r2, correctness NIT, second half) (PARTIAL: fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007; open: NUMBERING decided 2026-10-12 A (version check on Set). Done for the in-memory store, where) - The io_uring start helpers in
middleware/sse(sse_disconnect_linux_test.go:119-122,sse_engine_linux_test.go:100-105) andmiddleware/session(engine_roundtrip_test.go:83) skip on any start error that names io_uring, including ENOMEM fromio_uring_setup. At the CI runner's 8 MiB memlock the kernel charges ring memory per UID, so a start made while another package's test binary holds rings fails with ENOMEM although nothing is wrong, and the test skips. It happened toTestClientDisconnectCancelsStream/default/io_uring/{async,sync,h2c}in the Unit job's root step (run 37983851333, attempt 6, job 114026901221, on ci: forbid silent skips in the unit, adaptive and driver steps, run the #592 rig's io_uring half, and lift the ramp quarantine #961's head 780c920:engine unavailable on this runner: worker 0 ring setup: io_uring_setup: cannot allocate memory (likely RLIMIT_MEMLOCK; current=8388608 ...), three subtests at 0.02 s, with thectx-onlyarms of the same test passing in the same run). The same test alone at 8 MiB passed 5 of 5 with no skip. ci: forbid silent skips in the unit, adaptive and driver steps, run the #592 rig's io_uring half, and lift the ramp quarantine #961's root-step tally now re-runs such a test alone and requires it to pass, so CI is no longer red on it, but the run it lost was still a run in which the arm did not execute. Fix: retry ENOMEM for up to a few seconds asstartServer761andring_budget_linux_test.godo (epoll/io_uring: Listen publishes a nil Addr() and logs "engine listening" when boundAddr fails on loop 0 #639), and fail rather than skip when io_uring works. (from ci: forbid silent skips in the unit, adaptive and driver steps, run the #592 rig's io_uring half, and lift the ramp quarantine #961 review r1, CI) (Done: tests: start helpers retry ENOMEM and fail rather than skip; probe retry logs its rounds (checklist #888 items 3, 31, #890 item 6) #1047 [main])
Added 2026-10-09 (lane X1, PR #971):
- middleware/sse godoc says
Send's nil error in blocking mode means "the event was written" (middleware/sse/sse.go:69 on main b4ce620; the docs site page says the same, corrected in docs#95). On epoll, io_uring and adaptive over HTTP/1.1 a nil means the engine accepted the bytes for sending, and a write after the engine has noticed a reset is dropped without an error (TestStreamWriterWriteErrorAfterPeerGoneH1_875in docs: the tier-cap row separates Adaptive's automatic choice from an explicit start; StreamWriter.Flush is not a delivery guarantee on epoll and io_uring HTTP/1.1 #971). Reword to "handed to the connection" with a note that the disconnect arrives through the context. Found while correcting the same wording forStreamWriter.Flush(RULE 61). (from docs: the tier-cap row separates Adaptive's automatic choice from an explicit start; StreamWriter.Flush is not a delivery guarantee on epoll and io_uring HTTP/1.1 #971 review r1) (Done: sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 [main])
Added 2026-10-09 (lane M2, PR #972):
-
middleware/static
Compress:servePreCompressedandservePreCompressedFSpick the variant withstrings.Contains(ae, "br")/"gzip"on the rawAccept-Encodingvalue, soAccept-Encoding: br;q=0, gzip;q=0(the client refuses both) still gets the.br, on Root and on fs.FS (probelanes-20261009/M2/scripts/probe-q0_test.go.txt, loglogs/probe-q0.log, the fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 head; the substring test is the same on main b4ce620).Context.AcceptsEncodingsparses the q-values (it is what compress negotiates with); it also adds theVary, whichaddVarydoes by hand today. Not changed by fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972. (Done: static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022 [main]) -
middleware/compress in front of middleware/static over
FS(fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 at e120dca): a revalidation of a compressed response gets a304whoseETagdiffers from the one on the200. fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 makes theFSidentity tag strong;compressweakens a strong tag on the encoded200(W/"hash") but passes a304through, so it carries"hash"(probeTestReviewProbeCompress304Tag846, native loglogs/native-probe-compress-head.logof the fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 review:200: ce="gzip" etag=W/"d111d638...",revalidation: 304 etag="d111d638..."). RFC 9110 15.4.5 wants the 304 to carry the tag a200to the same request would have; RFC 9111 4.3.4 then stops a cache from using that 304 to freshen the weak-tagged stored response, so the cost is extra full fetches, not wrong bytes. On main both tags were weak and matched.compresscannot tell on a 304 (no body, no content type) whether it would have encoded the200(MinLength,ExcludedContentTypes, expansion), so weakening every strong tag of a 304 under a negotiated encoding would be wrong for the small and excluded files whose200stays strong; it needs a signal from the handler or a decision recorded per request. The same root cause asetag+compress. Not changed by fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 (stated under its "What this does not fix"). (from fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 review r1, minor) (Done: compress: a 304 carries the tag the 200 to the same request carries (static over fs.FS, etag) #1039 [main]) -
middleware/static
Root: a conditional request is answered with a304fromos.Statof the path, which follows a symlink out of the root beforeFileFromDirchecks containment. A request whoseIf-None-Matchequals the tag of an escaping symlink's target (W/"mtime-size"), or anIf-Modified-Sinceat or after its mtime, gets a304with that file'sETagandLast-Modified, so it tells whether a guessed mtime and size are right. It needs an escaping symlink planted insideRootand exposes metadata only, and it is the same on main b4ce620 (main also set the validators on the 400 and on plain requests; fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 stopped that, it no longer sets them on a response that is not a304or a send). Closing it means checking containment before the304, which costsFileFromDir's two symlink resolutions and an open (a Root304went from 1.6 us to 27.7 us when fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 tried it,bench/run-86db3fc-superseded-root-304-17x); anLstatof the path and its parents, oros.Root(Go 1.24+) for the lookup, may be cheaper. (from fix(static): a pre-compressed variant has its own validators, 304 and range; a strong fs.FS ETag; Root validators from the opened file (celeris#846) #972 review r1, minor) (Done: static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022 [main]) -
An SSE client that stays connected but stops reading, on epoll, io_uring and adaptive: nothing bounds it except the handler returning. With a 4 KiB receive buffer and 32 KiB events every 50 ms the handler is still running 30 s later (the mechanism was not traced: the native writes do not park the handler, and the idle deadline is not reached while events keep being queued). Decide whether a send-queue cap should close the conn, and add a test. std is bounded since fix(std): a detached stream (SSE) is not cut at ReadTimeout or WriteTimeout (#855) #998: each write of a detached stream gets WriteTimeout, and the writes fail when the drain budget runs out (std cells of
TestStalledSSEEnds*855). Evidence: lanes-20261009/SSE855/probes/zz_probe_stall_855_linux_test.go, logs/stall-fix.log (SSE855STALL lines). Related: A detached WebSocket connection has no liveness bound unless the app sets Config.IdleTimeout #524, sse: Broker.PublishPrepared under BrokerPolicyClose or BrokerPolicyRemove waits for a stuck subscriber's write, so one client that stops reading stalls the publisher #926. (from std engine: an SSE stream ends at the server's ReadTimeout/WriteTimeout (30 s in the bench adapter) while epoll, io_uring and adaptive keep it open; the docs say WriteTimeout: -1 is enough on every engine #855) (Done: epoll, iouring: a detached conn whose peer takes none of its queued bytes for WriteTimeout is closed (v1.6.0 polish items) #1077 [main]) -
sse.ClientPolicyClose(MaxQueueDepth 4) never ends the stream of a client that reads nothing on epoll, io_uring and adaptive:OnDisconnecthas not run 20 s after the client connected, on main and on fix(std): a detached stream (SSE) is not cut at ReadTimeout or WriteTimeout (#855) #998 (std is fine: it runs after the 2 s WriteTimeout). A bounded handler (returns after 3 s) on the same engines ends at 3.0 s while having pushed about 37 MiB to the unread socket, so the native writes buffer in user space without blocking; trace why the full queue does not close the client there. Found by the round 1 review of fix(std): a detached stream (SSE) is not cut at ReadTimeout or WriteTimeout (#855) #998. Evidence: lanes-20261009/SSE855/review-r1-pas/logs/evict-pr-v2.log, evict-off-v2.log. (from std engine: an SSE stream ends at the server's ReadTimeout/WriteTimeout (30 s in the bench adapter) while epoll, io_uring and adaptive keep it open; the docs say WriteTimeout: -1 is enough on every engine #855) (Done: epoll, iouring: a detached conn whose peer takes none of its queued bytes for WriteTimeout is closed (v1.6.0 polish items) #1077 [main]) -
internal/negotiate
ParseIntomatches the weight withstrings.HasPrefix(p, "q="), which is case-sensitive, soAccept-Encoding: gzip;Q=0is read as q=1 andcompress(throughContext.AcceptsEncodings) sends gzip to a client that refused it, with or without singleflight (alsoAcceptandAccept-Language). RFC 9110 5.6.6 and 12.4.2 make parameter names case-insensitive.gzip;q = 0(blanks around=) is not a valid weight in the grammar, so reading it as q=1 is arguably right; only the case of theqneedsstrings.EqualFoldon the first two bytes. Found by fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 review r1 (probeTestZZReviewCanonicalAEFoldsValuesTheNegotiatorTellsApart, leadergzip;Q=0->GZIP-BODYon main too). fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 makes the singleflight key follow the negotiator's reading, so the key stays consistent with whateverParseIntodoes. (from fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 review r1, nit) (Done: negotiate: a wildcard matches only what the header does not list; the q parameter name is case-insensitive #1043 [main]) -
middleware/cache
parseCacheControl:max-age=-1and a quotedmax-age="0"are ignored, so the response is stored for the fullConfig.TTL. RFC 9111 4.2.1 says a cache should treat an invalidmax-age(a non-delta-seconds value) as stale, i.e. do not store. Changing it stores less than today, so it is a behaviour change and was left out of fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 (which makesmax-age=0andno-cachenot stored).s-maxage(this cache is a shared one, so it applies and overridesmax-age) is the related gap already listed in the fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 body. (from fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 review r1, minor) (Done: fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 [main]) -
internal/negotiate
ParseIntois unbounded: it makes oneAcceptItemper list element, with no cap on the count or on the header's length, andAccept(soContext.AcceptsEncodings,AcceptsLanguages,Negotiate, andcompress) runs it on the client's whole header. AnAccept-Encodingofa,a,a,...costs about 60 to 70 times its length in heap: 3.8 MB for 64 KiB (the native engines' head limit), 1.2 GB for 16 MiB (the std engine's defaultMaxHeaderBytes). On origin/main, measured bylanes-20261009/MWCACHESF/scripts/probe-parseinto.sh(logs/r3/probe-parseinto-main.log: 59x, 62x, 72x). Found by fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 review r2 on the singleflight key, which fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 now keeps off the parser for a value over 256 bytes;compressand the other callers are not changed by it. Fix shape: stop reading entries after a small count (and treat a header over a few hundred bytes as having no preference), or parse without a slice; it is on the request path of every negotiating middleware, so it needs a RULE 74 measurement. (from fix(cache,singleflight): coalescing correctness and leader-path polish (v1.6.0 checklist items) #1007 review r2, major) (open: CODE-OTHER negotiate: a wildcard matches only what the header does not list; the q parameter name is case-insensitive #1043) -
TestLongAcceptEncodingCostsAboutItsLength888/16MiB(middleware/singleflight/long_accept_encoding_888_test.go) failed once in theCoverage (root + middleware sub-modules)job:defaultKeyFunc made 5.0 allocations for a 16777216-byte Accept-Encoding, want at most 2(run 38338177099, job 115102937737, PR adaptive: Metrics no longer waits behind a switch's listener wait; the wait lists again at once #1012 headbbb68ca, a change that touches nothing in middleware); the rerun of that job passed. The allocation check istesting.AllocsPerRun(3, ...)on a 16 MiB value, so a background goroutine's allocation (a server left by an earlier test of the package under the coverage binary) lands in the count. Make the check immune to that (count only the function's own allocations, or take the minimum over several runs, as for bytes) instead of rerunning. RULE 92: searched celeris, probatorium, docs and loadgen by mechanism (defaultKeyFunc allocations, Accept-Encoding allocations): no tracker. (Done: singleflight: the long Accept-Encoding allocation test no longer counts another goroutine's allocations #1021 [main]) -
internal/negotiate
Accept/isExcluded(found in PR static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022 review, minor, pre-existing): a*;q=0entry is put on the exclusion list and matches every offer, so a coding listed explicitly with q>0 is dropped:Accept-Encoding: gzip, *;q=0,br, *;q=0,*;q=0, gzipand RFC 9110 12.5.3's own examplegzip;q=1.0, identity; q=0.5, *;q=0select identity. RFC 9110 says*matches only codings not listed elsewhere in the field. With static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022 static's pre-compressed gate now goes throughAcceptsEncodings, so these headers get the identity file where main served the .gz/.br (a behaviour change to list in the release notes). Fix ininternal/negotiate(and tests forContext.AcceptsEncodings/Negotiate). (Done: negotiate: a wildcard matches only what the header does not list; the q parameter name is case-insensitive #1043 [main]) -
internal/negotiate
ParseInto: theqparameter name is matched case-sensitively (q=only), sobr;Q=0, gzipstill selects br; RFC 9110 12.4.2 says parameter names are case-insensitive. Same on main; theConfig.Compressdoc (static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022) says "an encoding the client refuses with q=0 is not served". (Done: negotiate: a wildcard matches only what the header does not list; the q parameter name is case-insensitive #1043 [main]) -
middleware/static (static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022 review, doc/test minors): the
doc.gosentence "the containment check thatContext.FileFromDirapplies" does not hold for Root.,./and/(resolvesInside's dot branch accepts an in-cwd index whileFileFromDirrequires the./prefix and refuses it, soGET /is a 400 without a validator and a 304 with one; not a leak, the file is inside the root); no test pins item 35 for a symlink under Root././(absolute and relative escaping index symlink, symlinked parent directory, each with If-None-Match tag,*, If-Modified-Since and no validator); the cost of a 304 under a symlinked directory that stays inside Root (Lstat walk plus twoEvalSymlinks) has no A/B row; oneConfig.Compressdoc line is not rewrapped. (open: TEST-HYGIENE static: honour Accept-Encoding q-values for pre-compressed variants; answer a Root 304 only for a path inside the root #1022) -
middleware/sse (sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 review r1, minor, pre-existing): no test checks that
ClientPolicyBlockqueues the copy rather than the caller's event. InsendQueuedthe block arm'scase c.queue <- e(afterOnSlowClientreturnedClientPolicyBlock) is covered by no ownership test:TestSlowClientCallbackKeepsItsEvent888covers drop and close only, and the native-engineTestSentEventSurvivesSendersNextRequestdoes not reach it. Mutant M3 (case c.queue <- orig, the uncopied event) passes the whole package, 192 PASS, 0 FAIL, 0 SKIP at sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023's earlier head. It does not fit thedropFullQueueharness, because the blocked Send waits on the gate that the harness opens in its defer. Option: a handler whoseOnSlowClientreturnsClientPolicyBlockand opens the gate itself (asTestSlowClientBlockPolicydoes), changes the caller's bytes while the Send is blocked, and then checks the wire. Evidence:lanes-20261009/MWSSE/review-r1-corr/mut/sse.go.M3,review-r1-corr/logs/controls.log. (from sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023 review r1) (open: TEST-HYGIENE sse: a dropped queued Send makes no copy; blocking-mode contract, stale comment and test knife-edge (v1.6.0 polish) #1023) -
tests (found by PR websocket: oracle test hardening (checklist #888 items 2, 3, 4, 5, 7, 11, 13) #1046 review r1, minor): the skip-on-
"io_uring"start helper survives in four more places, so a start that fails with ENOMEM (io_uring ring memory charged to RLIMIT_MEMLOCK by another test binary of the same user) skips there instead of retrying, and one that fails after the retry window should fail where io_uring works (see item 31, done for sse and session in PR tests: start helpers retry ENOMEM and fail rather than skip; probe retry logs its rounds (checklist #888 items 3, 31, #890 item 6) #1047 withclassifyStart):middleware/websocket/websocket_engine_linux_test.go(theContains(msg, "io_uring")skip at about :87),detached_count_window_linux_test.go(about :178),wsrst_torture_linux_test.go(about :89), andinternal/engine/epoll/detach_inline_regression_linux_test.go(about :131, another lane's directory). Fix shape: build the server and listener per try and retry ENOMEM for 10 s (asstartSSE732and PR tests: start helpers retry ENOMEM and fail rather than skip; probe retry logs its rounds (checklist #888 items 3, 31, #890 item 6) #1047'strySSEServer), then fail, skipping only where the probe says io_uring does not work and the message is not ENOMEM. RULE 92: searched goceleris/celeris, probatorium, docs and loadgen bygh search issues(skip io_uring start ENOMEM retry, waitUpOrStopped skip io_uring, RLIMIT_MEMLOCK ENOMEM start helper) and the four checkouts by grep (Contains(msg, "io_uring")): no tracker besides item 31. (open: TEST-HYGIENE tests: start helpers retry ENOMEM and fail rather than skip; probe retry logs its rounds (checklist #888 items 3, 31, #890 item 6) #1047)
Activity
test(api): allowlist later v1.x deprecations and catch a notice that …
FumingPower3925 commentedon Oct 3, 2026
Two items from the round-1 review of #901 (celeris#835, lane F3). #901 reclaims a response that was marked written only because a
StreamWriterwas taken; it does this in the router's error and panic paths (handleErrorandhandlePanic). Middleware sees that state before the router does:After fix(api): an error or panic before a taken StreamWriter sends anything is still answered, so an SSE OnConnect rejection reaches the client (celeris#835) #901, a taken but unused
StreamWriterstill makesc.IsWritten()returntrue. Middleware that branches on it therefore skips its own error handling:recoveryskips its configuredErrorHandlerand returnsErrPanicResponseCommitted, and the router's generic500goes out instead.timeoutreturnsErrServiceUnavailablewithout calling its error handler.if err != nil && !c.IsWritten()is skipped the same way.The client still gets an error response (sse: an OnConnect rejection never reaches the client: no response on epoll/io_uring/adaptive (the client hangs), an empty 200 on std #835's goal), but not the configured one. Measured on fix(api): an error or panic before a taken StreamWriter sends anything is still answered, so an SSE OnConnect rejection reaches the client (celeris#835) #901's round-1 head
3fcf625with the review's probe (evidence/lanes-20261003/F3/review-correctness-r1/probes/zz_review_835_probe_test.go, scriptfix-r1/scripts/28-835-mw-probe.sh, logfix-r1/logs/28-835-mw-probe.log, std engine):/rec-plain(panic, no writer taken) gets503 "custom recovery: boom"./rec(writer taken, then panic) gets500 "Internal Server Error"./mwgets500, and the middleware readIsWritten()=true.Fix: make
IsWritten()report the reclaimable state (taken, not used, not detached) as not written, or give middleware a way to reclaim it. This is a behaviour change, so it goes here and not into fix(api): an error or panic before a taken StreamWriter sends anything is still answered, so an SSE OnConnect rejection reaches the client (celeris#835) #901.Also after fix(api): an error or panic before a taken StreamWriter sends anything is still answered, so an SSE OnConnect rejection reaches the client (celeris#835) #901:
middleware/recovery/recovery.go:129-136logs "panic after response committed" and returnsErrPanicResponseCommittedwhen only aStreamWriterwas taken.handleErrorthen reclaims the response and answers500anyway, so the log line is wrong for that case. Reword the line, or skip it when the writer was never used. This is cosmetic, and it goes away if the item above is done.FumingPower3925 commentedon Oct 3, 2026
Two items from lane MW (the #719 and #721 audits, PRs #917 and #918):
RateFunc, every request whose rate string already has a limiter takes the dynamic-limiter map's write lock and clones the rate string (middleware/ratelimit/ratelimit.go:113-127at2ca5d72), only to re-assign the value-typed entry solastUsedmoves. Store*stoppableLimiterwith an atomiclastUsed, so a hit is one read-locked lookup with no allocation and no write lock. (fix(store, cache, ratelimit): an in-memory map keeps its own copy of every key it stores, so a retried Idempotency-Key replays on epoll and io_uring (celeris#719) #917 keeps the clone on that path and adds it to the insert and double-check paths, where it was missing.)Conn.LocalAddr()isnilon epoll and io_uring: the engine path has nonet.Conn, and the Context does not expose the connection's local address. Decide whether to expose it (the engines know it at accept), so the engine path answersLocalAddrthe way the hijack path does; fix(websocket): Conn.Param, IP and RemoteAddr answer from the upgrade request on every engine; every test helper that starts a server fails at once with Start's error (celeris#721, celeris#706) #918 documents thenilfor now.FumingPower3925 commentedon Oct 3, 2026
One item from the round-2 review of #902 (celeris#832, lane F3, correctness lens). Pre-existing: main does the same, and #902 restores main's 304.
304for a ranged request carries the206'sContent-Range. When a ranged request'sIf-None-Matchmatches the handler's tag,middleware/etag/etag.go(the 304 path,:91-101on fix(cache, compress, etag, singleflight): pass a 206 Partial Content through instead of storing, encoding, tagging or sharing it like a 200 (celeris#832) #902's headbedb2d3) answers304with every header the handler set,Content-Rangeincluded. RFC 9110 §15.4.5 says a304SHOULD NOT carry representation metadata other than the listed fields, and a cache that freshens its stored200with this304may copyContent-Rangeonto it. Measured with the review's probe (evidence/lanes-20261003/F3/review-correctness-r2/probes/zz_review_r2_etag_304_headers_test.go, logreview-correctness-r2/logs/native/etag-304-headers-probe.log): fix(cache, compress, etag, singleflight): pass a 206 Partial Content through instead of storing, encoding, tagging or sharing it like a 200 (celeris#832) #902's head and main'setag.goboth printstatus=304 headers=[[etag "v1"] [content-range bytes 0-3/10]]. Fix: dropContent-Range(and the other representation headers RFC 9110 §15.4.5 does not list) from the304, with a test on both a plain and a ranged request.FumingPower3925 commentedon Oct 3, 2026
New checklist item (lane SW, review of #916, correctness lens r1, part of its dispatch MAJOR that #916 does not remove by design):
middleware/pprof(andmiddleware/debugif any of its endpoints can block):/debug/pprof/profileand/debug/pprof/tracerun forseconds(30 by default) inside the handler, and the middleware runs wherever the request is dispatched. Under the sync default that is the engine worker, for the whole profile, for every connection the worker owns, whether pprof is mounted withUseor on a route. With fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916, underAsyncHandlersan unmatched request is dispatched like a route inheriting that default, with one adaptive key for all unmatched requests: a profile holds a worker on its first run, on the first run after each 5 s promotion TTL lapse, and, once fast unmatched traffic has settled that key, on every run until a settle re-open happens to time it (corrected 2026-10-03, issuecomment-5968203343; measured on v1.6.0 polish: adaptive engine #886, issuecomment-5968202543); with the sync default it always runs inline. A route can be marked.Async(), aUsemount cannot. Fix options: say so in the pprof doc (doc.go, docs observability.md) and name the mount that avoids it (s.GET("/debug/pprof/*path", h).Async()withceleris.Adapt, or a separate admin server), or have the middleware run the long endpoints off the worker itself (c.Detachand a goroutine). Code reading; the latency of the inline case is the reviewer's probe in fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916's body.FumingPower3925 commentedon Oct 3, 2026
Two items from lane MW's round-1 reviews (#920, #917); lines at
main4bb2042:ownStringValues(middleware/logger/logger.go:284-324) copies every string value a non-FastHandlerhandler gets, but not the attribute keys. Its doc says "Fields keys are expected to be constants", and nothing enforces that. AConfig.Fieldscallback (config.go:82-86) can returnslog.String(c.Header("x-key"), ...), and that key is then a receive-buffer view that a kept record reads later. fix(recovery, otel, singleflight, cache, sse, compress, api): copies for the request strings a log record, a span, a metric attribute set, a coalesced waiter or an SSE client keeps past the request (celeris#732, celeris#742) #920's otelappendOwnedcopies keys for this reason. Either copy the keys ofFieldsattrs into the same buffer, or say inFields' doc that keys must not be request strings.*stoppableLimiterwith an atomiclastUsed) also makes the clone that fix(store, cache, ratelimit): an in-memory map keeps its own copy of every key it stores, so a retried Idempotency-Key replays on epoll and io_uring (celeris#719) #917 adds at the double-check re-assign (middleware/ratelimit/ratelimit.go:142at fix(store, cache, ratelimit): an in-memory map keeps its own copy of every key it stores, so a retried Idempotency-Key replays on epoll and io_uring (celeris#719) #917's head) unnecessary. With pointer values, an existing entry is updated in place, so no map assignment ever re-keys it. (fix(store, cache, ratelimit): an in-memory map keeps its own copy of every key it stores, so a retried Idempotency-Key replays on epoll and io_uring (celeris#719) #917 review r1, NIT.)FumingPower3925 commentedon Oct 3, 2026
Two items from the round-2 reviews of #916 (celeris#852) and #915 (celeris#850), lane SW, and a correction to issuecomment-5967121541 above. Evidence paths are relative to
evidence/lanes-20261003/SW/.Correction to issuecomment-5967121541 (pprof's long endpoints; that comment is edited to match). It said that under
AsyncHandlersa profile holds a worker only "on its first run and on the first run after each TTL lapse". That understates it. All unmatched requests share one adaptive key. Once fast unmatched traffic has settled it, aUse-mounted pprof's profile runs inline on the worker every time, until a settle re-open happens to time it. With the sync default it always runs inline. The measurement and the dispatch items are on #886 (issuecomment-5968202543). The fix options in that item stand.Use-mounted circuitbreaker counts requests no route matches.IsError(status >= 500,middleware/circuitbreaker/config.go:66), a 404 or 405 is recorded as a success (circuitbreaker.go:173)./favicon.icoor a scanner's 404 can be the half-open probe that closes the breaker while the protected routes still fail, and 404 noise dilutes the failure ratio.zz_r2_circuitbreaker_unmatched_test.go(std), rerun on main 609a015 and fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916's head b4e7900 (fix-r2/scripts/02-verify-probes-darwin.sh→fix-r2/logs/02-verify/circuitbreaker-{main,h916}.log):/favicon.icoand 10 failing/upstream, the next/upstreamgets 503 on main and 500 with fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916, so one more failure is sent upstream;/favicon.icosent first: on main[open->half-open half-open->open](the next/upstreamwas the probe), with fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916[open->half-open half-open->closed], and then/upstream→ 500 reached the failing upstream.Skip: func(c *celeris.Context) bool { return !strings.HasPrefix(c.FullPath(), "/") }. With that Skip the probe behaves as on main: 503, and the breaker re-opens (fix-r2/logs/02-verify/cb-skip-h916.log).FullPathis a sentinel (<unmatched>,<method-not-allowed>,<options>) by default. It is small and in the family, but it is a behaviour change.Allowheader. RFC 9110 §15.5.6 says a 405 MUST.middleware/swagger/swagger.go:249(fix(swagger): no validator badge by default, serve the OAuth2 redirect page, a relative spec URL and redirect, Scalar's published file name (celeris#849, celeris#850, celeris#882, celeris#883) #915's head 53dff42) returnsceleris.NewHTTPError(405, "Method Not Allowed"). It is the onlyNewHTTPError(405inmiddleware/outside tests.{BasePath}/oauth2-redirect.htmland.js). With fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916, aUsemount also reaches this code for unmatched paths.fix-r2/probes/zz_fix2_swagger_405_test.go,fix-r2/logs/02-verify/swagger-405-{main,h916,h915}.log):POST /swagger/spec: with aUsemount, 404 on main and 405 with noAllowat fix(api): the global middleware runs for a request no route matches, so Use-mounted swagger, pprof, debug, healthcheck, metrics and static answer their paths (celeris#852) #916's head; with aPremount, 405 with noAllowalready on main;POST /swagger/oauth2-redirect.htmlwith aPremount at fix(swagger): no validator badge by default, serve the OAuth2 redirect page, a relative spec URL and redirect, Scalar's published file name (celeris#849, celeris#850, celeris#882, celeris#883) #915's head: 405 with noAllow.allow: GET, HEADbefore returning, and assert it inTestOAuth2RedirectPageServed850, which checks only the status. It is not in fix(swagger): no validator badge by default, serve the OAuth2 redirect page, a relative spec URL and redirect, Scalar's published file name (celeris#849, celeris#850, celeris#882, celeris#883) #915 because it changes a response header.perf(sf): one done channel per call replaces the WaitGroup and a goro…
test(cache): a follower of a canceled leader runs its own handler
fix(cache): a follower of a leader whose request context ended runs i…
test(cache): no-cache and max-age=0 responses are not stored
fix(cache): a no-cache or max-age=0 response is not stored; Cache-Con…
test(singleflight): waiters start no goroutine; a canceled leader's e…
196 remaining items
lint: De Morgan in the dotfile helpers and tests (Refs #888)
tests(static): round 2 of the security review: non-regular files (FIF…
fix(static, celeris): devices, FIFOs and sockets are not served; the …
static: the Config.AllowFilesystemRoot field, with no behaviour yet (…
tests(static, ctxkit): a Root that resolves to a filesystem root is r…
fix(static, celeris): a Root that resolves to a filesystem root is re…
tests(static): a conditional request for a FIFO or a socket is no 304…
tests(static): mkfifo behind build tags so the package vets on Window…
tests(static, celeris): round 3 of the security review: links that le…
fix(static, celeris): links that leave an FS are not followed under I…
tests(celeris): openNonblock does not wait on a FIFO (Refs #888)
tests(static): a FIFO as a directory's index file or as a pre-compres…
fix(static, celeris): every Open of a name from an FS is preceded by …
static, celeris: a Root of '.', './' and '/' serves the tree below it…
tests: the non-reading SSE tests' producers drop on a full queue befo…