🐛 fix(http): answer a failed request body as a client error - #2187
Open
gaborbernat wants to merge 2 commits into
Open
🐛 fix(http): answer a failed request body as a client error#2187gaborbernat wants to merge 2 commits into
gaborbernat wants to merge 2 commits into
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
gaborbernat
force-pushed
the
fix/client-body-failure-status-2184
branch
3 times, most recently
from
September 8, 2026 02:27
4628b0b to
c66f51a
Compare
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.
gaborbernat
force-pushed
the
fix/client-body-failure-status-2184
branch
from
September 8, 2026 03:30
c66f51a to
ca96d99
Compare
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.
A request body that failed answered
502with "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.502also 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.
BodyFailuresits inperyx-driver::bodybeside theStalledandTooSlowerrors 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 inperyx-driverrather thanperyx-httpbecause the registry takesperyx-httponly 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 answers400, since a body the server could not read fails the same way when a client retries it unchanged. Merging the first two into400fails 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
408satisfies as well as502did, 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, becauseshould_retry_statusalready retries bothis_server_error()andREQUEST_TIMEOUT, so it retries a408where it retried a502. RFC 9110 gives408the 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 treated502as fatal and408as retryable, or the reverse, would see a change this change cannot verify.This branch sits on #2186, which renamed
request_stall.rstorequest_bounds.rsandupload_stall_tests.rstoupload_bounds_tests.rs. The edits here are on the renamed files.Closes #2184