Format the failure dump instead of serializing it - #70
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.writeToreplaces the body with a rendering — a hex dump, cropped text, or a placeholder — then handed the result tohttp.Response.Write. That function honoursContent-LengthandTransfer-Encoding, which describe the body that arrived, not the rendering that replaced it. Reproduced onmaster: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 aTransfer-Encodingheader it invented:--silentis 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,
isPrintabletested onlyunicode.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.
And whitespace now counts as text, so bodies render as bodies:
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.goare replaced by positive assertions.Other Changes
^Min pagers and some CI log viewers. The request half keeps them: it is still serialized byhttp.Request.Write, which synthesizes theUser-AgentandContent-Lengthlines 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.writeTono longer reassignsres.Body. It was mutating the caller's response through a value receiver and closing a bodyClient.Dohad already closed.Notes for the reviewer
One gap is documented at
isPrintablerather than fixed: bytes that are not valid UTF-8 decode toU+FFFD, which is printable, so a body of high bytes still reads as text. The obviousutf8.Validguard 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 whetherContent-Lengthis present, not the encoding.Closes #18
🤖 Generated with Claude Code