Katkat

Debug a Toy HTTP Client That Hangs on a Redirected Streaming POST

StripeSoftware Engineer · Technical Screen

Last 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

  1. 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.
  2. Find the exact line(s) responsible.
  3. Fix it without changing the client's behavior for non-redirecting requests or plain-bytes bodies.
  4. 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?