Debug a Toy HTTP Client That Hangs on a Redirected Streaming POST
StripeSoftware Engineer · Technical ScreenLast updated
Find and fix a planted bug in a small hand-rolled Python HTTP client: a POST with a file-like body hangs forever after the server answers with a redirect, while a plain bytes body and a non-redirecting request both work fine.
You're handed a small, self-contained Python HTTP client (not a real third-party library — a toy one built for this exercise) with one reported bug. A reproduction looks like this:
client = ToyHttpClient(transport)
client.post("https://api.example.com/v1/charge", body=io.BytesIO(b"amount=5000"))
# the server responds 307 to a new URL, and the request to that new URL never completes
The same call with body=b"amount=5000" (plain bytes) completes normally, and the file-like body also completes normally when the server doesn't redirect. Only the combination of a file-like body and a redirect hangs — in production this means the client waits out its full timeout (or forever, with none set) on every redirected POST that streams its body from a file-like object.
What you're given
A ToyHttpClient with post(url, body) that: builds a request (computing a Content-Length header by seeking to the end of a seekable body and back), sends it over a transport, and if the response is a 3xx with a Location header, resends the same prepared request to the new URL, repeating until a non-redirect status or too many redirects.
Your task
- Explain precisely why the hang only happens for a file-like body after a redirect, in terms of what each side has sent and is waiting for.
- Find the exact line(s) responsible.
- Fix it without changing the client's behavior for non-redirecting requests or plain-bytes bodies.
- Write a deterministic regression test that reproduces the hang against the buggy code (without an actual multi-second sleep or real socket) and proves the fix resolves it.
Constraints
- Don't change how redirects are followed in general, only the body-resend bug.
- The body in the reproduction is seekable with a known length, so it's sent with
Content-Length, not chunked encoding. - Assume the redirecting server itself behaves correctly and is not the bug.
Hints
Hint 1
Compare what the second request's Content-Length header promises against what the client actually has left to send from the body object at that point.
Hint 2
A file-like object has a read position that a .read() call moves forward. What has already happened to that position by the time the client resends?
Hint 3
The fix isn't 'always seek to 0' — it's remembering exactly where the body started (which might not be 0) and returning to that exact position before every send, not just the first.
Hint 4
Build your regression test's fake transport so it can detect the under-delivery (fewer bytes than Content-Length promised) and raise instead of actually blocking, so the test runs instantly and deterministically.
Follow-up questions
- What should happen if the body is a generator or pipe that genuinely can't be rewound — is silently resending nothing acceptable, or should that be a hard error?
- Does the same class of bug exist for a redirect from an HTTP/1.1 keep-alive connection versus a fresh connection per request, or is it identical either way?
- How would you detect this class of bug with a fuzz or property-based test instead of one hand-written reproduction?
- If the redirect target is on a different host, should the body still be resent at all, and does your fix need to change for that case?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.