Skip to content

Format the failure dump instead of serializing it - #70

Merged
korya merged 2 commits into
masterfrom
korya-fix-failure-dump
Aug 8, 2026
Merged

Format the failure dump instead of serializing it#70
korya merged 2 commits into
masterfrom
korya-fix-failure-dump

Conversation

@korya

@korya korya commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Problem

The dump you read when an assertion fails was being produced by an HTTP wire serializer, so it destroyed the thing it was meant to show.

httpResponse.writeTo replaces the body with a rendering — a hex dump, cropped text, or a placeholder — then handed the result to http.Response.Write. That function honours Content-Length and Transfer-Encoding, which describe the body that arrived, not the rendering that replaced it. Reproduced on master:

$ http-assert -s --assert-ok http://…/500          # body "boom", Content-Length: 4
  <<                                                # ← "  << Payload is omitted >>", cut to 4 bytes

$ http-assert -v --assert-body-eq x http://…/binary # 8 raw bytes
00000000                                            # ← the entire hex dump

A chunked response has no Content-Length, so instead of truncating it re-framed the output — a hex chunk length, the text, a zero terminator, and a Transfer-Encoding header it invented:

Transfer-Encoding: chunked

18
first chunk second chunk
0

--silent is the mode the README's own monitoring example recommends, so the mode most likely to be deployed was the mode that hid the diagnostic.

Separately, isPrintable tested only unicode.IsPrint, which answers false for \n. Any body with a line break in its first 256 bytes — pretty-printed JSON, HTML, a log excerpt — was classified as binary and hex-dumped.

Solution

Render the dump directly: status line, sorted headers, blank line, body. No serializer in the path means no length or framing can disagree with what is printed.

HTTP/1.1 500 Internal Server Error
Content-Length: 4
Content-Type: text/plain; charset=utf-8

  << Payload is omitted >>

And whitespace now counts as text, so bodies render as bodies:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8

line one
line two
line three

Control bytes still route to the hex dumper — a body mixing newlines with a NUL is binary and reads as binary.

Go had been reporting the truncation on every single failing run (http: ContentLength=4 with Body length 26); _ = r.Write(w) discarded it. There is nothing left to discard.

Coverage stays at 100.0%. The renderer gets in-process unit tests rather than relying only on the 12-second end-to-end suite, and the two characterization tests in e2e_known_issues_test.go are replaced by positive assertions.

Other Changes

  • The response half of the dump drops wire line endings, which render as ^M in pagers and some CI log viewers. The request half keeps them: it is still serialized by http.Request.Write, which synthesizes the User-Agent and Content-Length lines the dump shows. Replacing it belongs with Request body is never shown in the failure dump (and Content-Length contradicts it) #19, which has to rebuild that half anyway to recover the missing request body.
  • writeTo no longer reassigns res.Body. It was mutating the caller's response through a value receiver and closing a body Client.Do had already closed.

Notes for the reviewer

One gap is documented at isPrintable rather than fixed: bytes that are not valid UTF-8 decode to U+FFFD, which is printable, so a body of high bytes still reads as text. The obvious utf8.Valid guard is wrong here — cropping happens before the check and can split a multi-byte rune, which would misfile legitimate multi-byte text as binary. Worth its own issue.

The issue's third reproduction (a gzip'd body) does not reproduce as filed: Go's transport transparently decompresses, which sets ContentLength = -1, so nothing truncates. Same root cause, mis-attributed trigger — the deciding factor is whether Content-Length is present, not the encoding.

Closes #18

🤖 Generated with Claude Code

korya and others added 2 commits August 7, 2026 22:48
The dump a person reads when an assertion fails was produced by
http.Response.Write, an HTTP wire serializer. It honours Content-Length and
Transfer-Encoding, which describe the body that arrived -- not the rendering
that replaces it before printing. Two consequences, both silent:

A rendering longer than the original body was cut to the original's length.
A four-byte body reduced "  << Payload is omitted >>" to "  <<", and an
eight-byte binary body reduced its hex dump to the offset "00000000",
from a dumper whose entire purpose is showing the bytes. --silent is the
mode the README's monitoring example recommends, so the mode most likely to
be deployed was the mode that hid the diagnostic.

A chunked response has no Content-Length, so instead of truncating, the
serializer re-framed the rendering: a hex chunk length, the text, a zero
terminator, and a Transfer-Encoding header it invented. Protocol framing in
a human-readable report.

Go reported the first case every single time -- "http: ContentLength=4 with
Body length 26" -- and the caller discarded the error. There is nothing to
discard now: the renderer emits the whole output itself, so no length can
disagree with it.

The response half also drops wire line endings, which show up as ^M in
pagers and some CI log viewers. The request half keeps them for now; it is
still serialized by http.Request.Write, which synthesizes the User-Agent and
Content-Length lines the dump shows. Replacing it belongs with #19, which
has to rebuild that half anyway to recover the request body.

writeTo no longer reassigns res.Body. It was mutating the caller's response
through a value receiver, and closing a body Client.Do had already closed.

Closes #18

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
unicode.IsPrint answers false for '\n', '\t' and '\r', and isPrintable
tested nothing else. So a body with a line break anywhere in its first 256
bytes was classified as binary and sent to the hex dumper:

    $ http-assert -v --assert-body-eq x http://…/multiline
    00000000  6c 69 6e 65 20 6f 6e 65  0a 6c 69 6e 65 20 74 77  |line one.line tw|

That is pretty-printed JSON, HTML, and any log excerpt -- most of what a
response body actually contains -- rendered in the least readable form
available, at the moment somebody is reading it to find out what went wrong.

Whitespace is text, so the classifier now says so. Control bytes still route
to the dumper: a body mixing newlines with a NUL is binary and reads as
binary.

Known gap, left alone deliberately and documented at the function: bytes
that are not valid UTF-8 decode to U+FFFD, which is printable, so a body of
high bytes still reads as text. The obvious utf8.Valid guard is wrong here
because cropping happens first and can split a multi-byte rune, which would
misfile legitimate text as binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMMhgmTkbzAsmeNy97PrP
@korya
korya marked this pull request as ready for review August 8, 2026 02:57
@korya
korya merged commit c955587 into master Aug 8, 2026
8 checks passed
@korya
korya deleted the korya-fix-failure-dump branch August 8, 2026 02:57
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.

Failure dump truncates or destroys the response body (Content-Length is not updated)

1 participant