Skip to content

rest: gzip FlushError + Unwrap — ResponseController works behind gzip (#167)#171

Merged
SyniRon merged 4 commits into
developfrom
agent/issue-167
Jun 7, 2026
Merged

rest: gzip FlushError + Unwrap — ResponseController works behind gzip (#167)#171
SyniRon merged 4 commits into
developfrom
agent/issue-167

Conversation

@SyniRon

@SyniRon SyniRon commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #167.

gzipResponseWriter supported no ResponseController verb — flush dead-ended at the gzip layer with ErrNotSupported, blocking any future streaming handler. Adds FlushError (gzip buffer first — sync block keeps the wire a valid gzip prefix — then the underlying chain) and Unwrap (deadline-class verbs pass through; the controller's method search prefers the explicit FlushError, so flush can never bypass the gzip buffer — verified against the installed stdlib). Explicitly not a bare Unwrap, which would corrupt the stream. Supported-verb decisions documented on the type.

Review-driven hardening in-branch:

  • Fixed before merge: FlushError now strips stale uncompressed Content-Length (review found flush-before-first-write committed it → silently truncated stream, flush and Write both returning nil; verified by live repro, pinned RED-first, Del-placement mutants killed)
  • Mid-body flushed prefix pinned incrementally decodable over a real conn; final body byte-identical through intact trailer; non-gzip passthrough pinned
  • Unflushable chain still fails loud; partial-write-on-failure half-state pinned at flush time (in-handler snapshot, ≥15 bytes + gzip magic — probe-first mutants killed)
  • Streaming test hang mode closed (request deadline; revert fails in 10s, not the global timeout); 20× -race no flakes
  • cacheControlWriter rollback comment re-scoped — its "nothing reached the wire" premise no longer holds below gzip

Two review rounds; remaining pre-existing findings (WriteHeader stale-CL hole, gateway statusRecorder) deferred to tracked issues.

🤖 Generated with Claude Code

SyniRon and others added 4 commits June 7, 2026 10:44
…#167)

gzipResponseWriter implemented neither Flush/FlushError nor Unwrap, so
http.NewResponseController(w).Flush() on any gzip-negotiated request
died with ErrNotSupported and every other controller verb was equally
unreachable — a future streaming handler could never flush behind gzip.

FlushError flushes the gzip.Writer first (sync block: everything written
so far becomes a decodable gzip prefix on the wire) THEN the underlying
chain — never the reverse, and never a bare Unwrap for flush, which
would bypass the gzip buffer and corrupt the stream. Unwrap passes the
non-stream verbs (deadline control) through; the controller's method
search prefers the explicit FlushError, so flush can't take that route.

Pinned behaviorally over real server connections (the recorder is a
Flusher and would mask a dead tunnel): flush succeeds when negotiated,
mid-body flushed prefix decodes incrementally while the handler still
holds the tail, full body stays byte-identical through the trailer,
non-negotiated requests untouched. Internal pins take a wire-faithful
snapshot at the moment the delegated flush fires (crisp ordering
witness) and keep an unflushable chain failing loud (ErrNotSupported
propagates). Stale "gzip chain can never flush" notes in cachecontrol
truth-ed up; its rollback stays — any future unflushable wrapper
reopens that hole.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mutation-validating the flush pins exposed a harness hang: when the
prefix-decode watchdog fired, the handler stayed blocked on release and
the deferred srv.Close waited on it forever. A deferred sync.OnceFunc
release (LIFO, before Close) turns that regression shape into a crisp
failure instead of a suite hang.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, harden tests (#167)

Post-review fixes:
- FlushError now dels the uncompressed Content-Length before emitting the
  sync block — flush is a header-committing event, and the stale length
  truncated the compressed stream silently (flush AND Write returned nil).
  Pinned RED-first with a real-server flush-before-first-write test.
- cacheControlWriter rollback comment scoped: 'nothing reached the wire'
  holds above gzip, not below — gzip's FlushError writes header+sync block
  downstream before the delegated flush can fail. Logic unchanged.
- FlushError doc + internal test now pin the partial-write-on-failure
  property (ErrNotSupported is not a no-op).
- Streaming test gets a request-context deadline so a flush mutant fails
  in seconds instead of hanging Do to the global go-test timeout.
- Comment corrections: real reasons for real connections in the package
  doc, honest Hijack consequences (deferred gz.Close fires ErrHijacked +
  misleading truncation log), rest_ -> tail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixup round 2 from review:

- gzip_internal_test.go: snapshot the underlying buffer INSIDE the
  handler the moment FlushError returns — after ServeHTTP the deferred
  gz.Close() writes header+trailer regardless, so the old post-return
  assert.Positive passed even for a probe-first mutant that pushes
  nothing (mutation-proven vacuous). Now pins >=15 bytes (10-byte gzip
  header + 5-byte sync block) and the 1f 8b magic at flush-failure
  time. Also corrected the skip-flush-mutant comment: its snapshot
  holds the 10-byte header, and the decode fails at io.ReadFull, not
  gzip.NewReader.

- gzip_test.go: dropped the false "only the streaming decode … catches
  it" exclusivity (the internal snapshot test kills the same mutant);
  the true claim is no error assertion can catch it — the catch must
  decode what was delivered at flush time. Added the missing fourth
  recorder limitation (only net/http enforces a declared
  Content-Length), and fixed the watchdog/deferred-release arming
  description (registered before Do, fires only after).

- gzip.go: scoped the "not a no-op" sentence to the delegated-flush
  failure path (a gz.Flush failure leaves an arbitrary prefix), and
  noted the pre-existing explicit-WriteHeader stale-CL hole the Del
  cannot cover, tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rest: gzipResponseWriter supports no flush and no Unwrap — ResponseController dead-ends on the gzip chain

1 participant