Fix an HTTP Client That Loses a Stream Body on Redirect Replay
StripeSoftware Engineer · OnsiteLast updated
Debug a small HTTP client library where POST requests with a plain string body replay correctly after a redirect, but requests with a file-like stream body hang, and fix the library so a redirect resends the exact original bytes, including bytes from a stream the caller had already partly consumed.
You're working on FeatherHTTP, a small internal HTTP client library. When a server answers a POST with a 307 Temporary Redirect, the client is supposed to resend the same request, same method, same body bytes, to the new location. Three tests exercise this. The one that posts a plain string passes. The two that post a seekable, file-like stream (io.BytesIO) as the body hang and eventually time out.
The library accepts three shapes of data for a POST: a plain string, a fresh io.BytesIO stream, and a stream the caller has already partly read from before handing it to the client — in that last case, only the remaining, unread bytes count as the body.
What the three cases must produce
data argument | Caller's state before the call | Bytes the server must receive on both the first attempt and the redirected one |
|---|---|---|
"ping" | n/a | ping |
io.BytesIO(b"ping") | stream position 0 | ping |
io.BytesIO(b"pingpong") after .read(4) | stream position 4 | pong |
The failing tests
def test_redirect_replays_string_body(transport):
result = post_through(transport, "/start", data="ping")
assert result == b"ping"
def test_redirect_replays_fresh_stream_body(transport):
result = post_through(transport, "/start", data=io.BytesIO(b"ping"))
assert result == b"ping"
def test_redirect_replays_partially_read_stream_body(transport):
stream = io.BytesIO(b"pingpong")
stream.read(4)
result = post_through(transport, "/start", data=stream)
assert result == b"pong"
transport is a fake in-process server: posting to /start always answers with a 307 pointing at /target; posting to /target echoes back whatever body bytes it received, or raises a TimeoutError if the body came through empty (standing in for a real server hanging while it waits for bytes that a Content-Length header promised but that never arrive).
Your task
Figure out why the plain-string case works while both stream cases fail, and fix the library so all three tests pass, including the constraint that a stream the caller already partly read must replay only its remaining bytes, both on the first send and on the redirected one.
Hints
Hint 1
Trace exactly what object gets handed to the code that sends the redirected request. Is it the original `data`, or something already derived from it?
Hint 2
A string behaves differently than a stream here because reading a string twice gives you the same string, but reading a stream twice does not give you the same bytes.
Hint 3
Decide the one moment you need to remember a stream's starting read position, and make sure you capture it before anything reads from the stream, not after.
Hint 4
A stream without `seek`/`tell` can't be replayed this way at all — decide what the library should do when it's handed one.
Follow-up questions
- How would this design need to change for a stream that can't be seeked at all, like data arriving live from a pipe?
- If the redirect target uses a different HTTP method than the original request (a 303 rather than a 307), how does that interact with whether the body should be resent at all?
- How would you add a size limit so an extremely large stream body doesn't get buffered entirely in memory just to make it replayable?
- What test would you add to catch a future regression where someone reads from `self.raw` directly somewhere else in the library, bypassing `PreparedBody`?
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.