Skip to content

Order the gzip hijack error path so failures cannot corrupt the body - #56

Merged
umputun merged 1 commit into
masterfrom
fix/gzip-hijack-error-path
Aug 19, 2026
Merged

Order the gzip hijack error path so failures cannot corrupt the body#56
umputun merged 1 commit into
masterfrom
fix/gzip-hijack-error-path

Conversation

@paskal

@paskal paskal commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #55, addressing the error-path ordering umputun raised there.

hijack closed the gzip stream, discarded the Close error, and released the writer to the pool, all before attempting the hijack:

if w.gz != nil {
    _ = w.gz.Close()
    gzPool.Put(w.gz)
    w.gz = nil
}
conn, rw, err := h.Hijack()

Two things followed from that.

A failing Close was invisible. It means truncated gzip output, and a hijack succeeding immediately after hid the fact. The error is returned now, wrapped.

A failing Hijack could corrupt the response. hijacked stayed false while w.gz was already nil, so a handler that carried on writing took the w.ResponseWriter.Write path and appended raw bytes to a body already carrying Content-Encoding: gzip. On that path the writer now stays attached, and because it is closed a further write fails with flate: closed writer instead. Verified rather than assumed: a closed gzip.Writer returns that error and leaks nothing into the output.

The writer is released only once Hijack has actually taken the connection. On the failure path the deferred close still returns it to the pool, which is safe because Reset clears the closed flag, and a second Close on an already-closed writer returns nil.

Three cases in the new test, covering the failing close, the failed hijack followed by a write, and the successful hijack releasing the writer. The first two fail on the current code.

Narrow in practice, as noted on #55Hijack failing after the type assertion succeeded is mostly a double hijack — but the ordering is strictly safer and costs nothing.

…body

Closing the stream discarded its error and released the writer before the
hijack was attempted, which cost two things.

A failing Close means truncated gzip output, and a hijack succeeding right
after hid it. That error is returned now.

A failing Hijack left hijacked false with the writer already released, so a
handler carrying on would take the plain path and append raw bytes to a body
already advertised as Content-Encoding: gzip. The writer stays attached on that
path instead, and since it is closed a further write fails with "flate: closed
writer" rather than corrupting the response. The deferred close still returns it
to the pool, and the pool is safe because Reset clears the closed flag.

The writer is only released once Hijack has actually taken the connection.

Reported by umputun on #55.
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 32201953393

Coverage increased (+0.2%) to 97.186%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 14 of 14 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1706
Covered Lines: 1658
Line Coverage: 97.19%
Coverage Strength: 47.32 hits per line

💛 - Coveralls

@umputun
umputun merged commit 153b694 into master Aug 19, 2026
6 checks passed
@umputun
umputun deleted the fix/gzip-hijack-error-path branch August 19, 2026 01:26
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.

3 participants