Skip to content

🐛 fix(http): answer a failed request body as a client error - #2187

Open
gaborbernat wants to merge 2 commits into
mainfrom
fix/client-body-failure-status-2184
Open

🐛 fix(http): answer a failed request body as a client error#2187
gaborbernat wants to merge 2 commits into
mainfrom
fix/client-body-failure-status-2184

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Sep 4, 2026

Copy link
Copy Markdown
Member

A request body that failed answered 502 with "upstream transfer failed". Nothing upstream serves a request body: the bytes were coming from the client and peryx is the server. The bounds the edge now puts on an inbound body, the stall bound from #2182 and the throughput floor from #2186, made it reachable with the client still connected, so a client that paused for thirty seconds, or one that trickled below the floor, heard that something upstream of peryx had gone wrong. 502 also tells an intermediary that a backend gave a bad answer, which invites a retry against a different backend for a condition no backend change reaches. 🔍

The distinction lives in one place. BodyFailure sits in peryx-driver::body beside the Stalled and TooSlow errors the edge produces, and both ecosystems ask it the same question instead of each reading an opaque body error. A handler cannot recover the distinction on its own: the error arrives boxed and wrapped by whatever read the body, so the classification walks the source chain instead of inspecting the outermost error. Putting it anywhere else would have meant two answers to one question, which is the per-handler patchwork #2176 exists to avoid. It landed in peryx-driver rather than peryx-http because the registry takes peryx-http only as a dev-dependency, while both ecosystems already depend on the driver at runtime.

I chose the three statuses for what they tell the client to do next. A body that stopped arriving and a body that fell short of the floor both answer 408, which says the server gave up waiting for a message that never arrived: neither request completed, so the client is free to send it again, the first as it stands and the second over a link that can sustain the transfer. A registry upload also gets the offset its session reached, so it resumes at that point. Everything else answers 400, since a body the server could not read fails the same way when a client retries it unchanged. Merging the first two into 400 fails in the more damaging direction. It tells a client that stalled that its request came out malformed, so the useful move, sending it again, looks pointless. ✨

I updated the registry cases that asserted the old status, and their intent survives. The bound cases asserted that a cut chunk must not read as accepted, which 408 satisfies as well as 502 did, and the message that now accompanies them says which end went quiet and where the session stands. The monolithic case took its name from the gateway status it happened to produce rather than from any gateway being involved, so it now names a client error.

On what changes for clients that retry the old 502: peryx's own upstream client sees no change, because should_retry_status already retries both is_server_error() and REQUEST_TIMEOUT, so it retries a 408 where it retried a 502. RFC 9110 gives 408 the same permission, since the request never completed and the client may retry it. What an external pusher such as docker or containerd does with either status is not something this repository can answer, so a client that treated 502 as fatal and 408 as retryable, or the reverse, would see a change this change cannot verify.

This branch sits on #2186, which renamed request_stall.rs to request_bounds.rs and upload_stall_tests.rs to upload_bounds_tests.rs. The edits here are on the renamed files.

Closes #2184

@gaborbernat gaborbernat added the bug Something isn't working label Sep 4, 2026
@codspeed-hq

codspeed-hq Bot commented Sep 4, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 29 untouched benchmarks
⏩ 133 skipped benchmarks1


Comparing fix/client-body-failure-status-2184 (3a20033) with main (bb9270a)

Open in CodSpeed

Footnotes

  1. 133 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@read-the-docs-community

read-the-docs-community Bot commented Sep 4, 2026

Copy link
Copy Markdown

Documentation build overview

📚 peryx | 🛠️ Build #34445531 | 📁 Comparing 3a20033 against latest (e64cc83)

  🔍 Preview build  

No files changed.

@gaborbernat
gaborbernat force-pushed the fix/client-body-failure-status-2184 branch 3 times, most recently from 4628b0b to c66f51a Compare September 8, 2026 02:27
A request body that failed answered 502 with "upstream transfer failed". No
upstream serves a request body: the bytes come from the client and peryx is the
server. The bounds the edge now puts on an inbound body made it reachable with
the client still connected, so a client that paused, or that trickled below the
floor, was told something upstream of peryx had gone wrong. 502 also tells an
intermediary that a backend gave a bad answer, which invites a retry against a
different backend for a condition no backend change reaches.

The distinction has to be made once. A handler holding the body error cannot
see which failure it has, because the edge boxes it and whatever read the body
wraps it again, so each handler that tried would re-derive an answer from an
opaque error and they would drift. BodyFailure sits beside the errors the edge
raises and walks that chain, and both ecosystems ask it rather than guessing.
It lives in peryx-driver because the registry takes peryx-http only as a
dev-dependency while both ecosystems already depend on the driver.

The statuses follow what the client should do next. A body that stopped, and
one that never kept up, both mean the server gave up on a message that never
arrived, which is what 408 says, and both leave the client free to send it
again; a registry upload also gets the offset its session stands at. Anything
else answers 400, since a body the server could not read fails the same way
when repeated. Folding the first two into 400 would have been the more damaging
merge: it tells a client that stalled that its request was malformed, so the
move that would work looks pointless.

The tests that asserted the old status keep their intent. The bound cases
asserted that a cut chunk must not read as accepted, which 408 satisfies, and
the monolithic case took its name from the status it happened to produce rather
than from any gateway being involved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report a client-side body failure as a client error

1 participant