Skip to content

fix(requests): quote only the received body in status errors - #1

Merged
memclutter merged 1 commit into
mainfrom
fix/exec-short-body-panic
Aug 12, 2026
Merged

fix(requests): quote only the received body in status errors#1
memclutter merged 1 commit into
mainfrom
fix/exec-short-body-panic

Conversation

@memclutter

Copy link
Copy Markdown
Owner

What

Both status-mismatch branches in Exec() built their error as
res.Status + string(body[:50]) and passed the result as the format argument
to fmt.Errorf. They now use a constant format string and quote exactly the
received body, truncated at maxErrorBodyLen (512) with an marker.

return fmt.Errorf("unexpected response status %s: %s", res.Status, truncateBody(body, maxErrorBodyLen))

Why

Two defects, both measured against the shipped code:

  1. body[:50] reads past len(body). It does not panic, contrary to what
    one might expect: io.ReadAll always allocates with cap >= 512, so the
    re-slice stays inside the allocation and exposes the read buffer's slack
    instead. A {"error":"x"} body rendered as 404{"error":"x"} followed by 37
    NUL bytes; an empty body as 404 followed by 50 NUL bytes. The padding is
    NUL today only because the buffer is freshly allocated — the code has no
    right to those bytes either way.
  2. The body was the format string. A 100% failed body rendered as
    100%!f(MISSING)ailed.

The 512-byte bound is new (the old 50 was an accident of the broken slice); it
keeps a typical JSON error payload intact while stopping a multi-megabyte HTML
error page from landing in a log line verbatim.

Tests

  • TestTruncateBodylen == 0, len < limit, len == limit,
    len == limit + 1; the marker appears only in the last case.
  • TestExecStatusError — 4 bodies (empty, short JSON, one containing %, one
    of 2×maxErrorBodyLen) × 2 assertion paths (ResponseCodeOk vs 404,
    ResponseCodeFail vs 500) = 8 cases. Asserted by equality on the full
    message, not Contains: the defect is extra bytes appended to the body,
    which a substring check would pass.

Red-first check: with the two branches reverted to the old expression, all 8
TestExecStatusError cases fail.

Local gates: go test -count=1 ./... (40 tests), go vet ./... — clean, which
is itself part of the proof, since vet flags the old non-constant format string
— and gofmt -l ..

Notes

  • No API change; truncateBody and maxErrorBodyLen are unexported, so
    gorequests-proxy and gorequests-retry are unaffected.
  • The error message text changes. Nothing in the three modules asserts on it.
  • Stays within go 1.18; io/ioutil and the Go baseline are left alone
    deliberately (separate modernisation task).
  • Found in passing, not fixed here: Url(format, args...) runs the URL through
    fmt.Sprintf, so a % in the URL itself (e.g. percent-escaping) is mangled
    into %!f(MISSING).

🤖 Generated with Claude Code

The status-mismatch branches built their message as
res.Status + string(body[:50]) and passed it as the format argument to
fmt.Errorf. Two defects:

- body[:50] reads past len(body) on bodies shorter than 50 bytes. It does
  not panic, since io.ReadAll returns a slice with cap >= 512, so the
  re-slice stays inside the allocation and exposes the buffer's slack
  instead: a {"error":"x"} body rendered as 404{"error":"x"} plus 37 NUL
  bytes.
- A body containing % was interpreted as a format verb, so 100% failed
  rendered as 100%!f(MISSING)ailed.

Both branches now use a constant format string and quote exactly the
received body, truncated at maxErrorBodyLen (512) with an ellipsis marker
so a large HTML error page cannot land in a log line verbatim.
@memclutter
memclutter merged commit ff55668 into main Aug 12, 2026
5 of 6 checks passed
@memclutter
memclutter deleted the fix/exec-short-body-panic branch August 12, 2026 13:23
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.

1 participant