Fix Seekable POST Body Replay After a 307 Redirect in Python requests
Company: Stripe
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Onsite
You are working in a checkout of the open-source Python HTTP library `requests`. When a server answers a POST with `307 Temporary Redirect`, the client must repeat the request at the new location with the same method, POST, and the same body bytes it sent the first time. Three tests check this. The test that posts a plain string passes, but the two tests that post a seekable stream (`io.BytesIO`) hang after the redirect and fail with a read timeout.
Find out why the plain string works while the seekable streams time out, then fix the library so that all three tests pass. The fix must also support a stream that the caller has already partly read before calling `requests.post`: in that case the body is only the bytes from the caller's position onward.
| `data` passed to `requests.post` | State before the call | Body that both the first POST and the redirected POST must send |
|---|---|---|
| `'test'` | plain string, no stream position | `test` |
| `io.BytesIO(b'test')` | position 0 | `test` |
| `io.BytesIO(b'hello world')` after `read(5)` | position 5 | ` world`, keeping the leading space |
The tests you are given:
```python
def test_HTTP_307_ALLOW_REDIRECT_POST_DATA(self, httpbin):
r = requests.post(httpbin('redirect-to'), data='test', params={'url': httpbin('post'), 'status_code': 307}, timeout=5)
assert r.status_code == 200
assert r.history[0].status_code == 307
assert r.history[0].is_redirect
assert r.json()['data'] == 'test'
def test_HTTP_307_ALLOW_REDIRECT_POST_WITH_SEEKABLE(self, httpbin):
byte_str = b'test'
r = requests.post(httpbin('redirect-to'), data=io.BytesIO(byte_str), params={'url': httpbin('post'), 'status_code': 307}, timeout=5)
assert r.status_code == 200
assert r.history[0].status_code == 307
assert r.history[0].is_redirect
assert r.json()['data'] == byte_str.decode('utf-8')
def test_HTTP_307_ALLOW_REDIRECT_POST_WITH_PARTIAL_SEEKABLE(self, httpbin):
data = io.BytesIO(b'hello world')
data.read(len("hello"))
r = requests.post(httpbin('redirect-to'), data=data, params={'url': httpbin('post'), 'status_code': 307}, timeout=5)
assert r.status_code == 200
assert r.history[0].status_code == 307
assert r.history[0].is_redirect
assert r.json()['data'] == ' world'
```
The `httpbin` fixture is a local test server: `redirect-to` answers with the requested status code and redirects to `url`, and `post` echoes the request body it received in the `data` field of its JSON response.
```hint Compare the two bodies
Ask what state an `io.BytesIO` object carries that a string does not, and what sending the first request does to that state.
```
```hint Follow one request object
Trace the request from the first send, through the code that builds the redirected request, to the second send. Note which objects the redirected request shares with the original one.
```
### Constraints and Clarifications
- Fix the library, not the tests. All three tests must pass unchanged.
- Redirects with status 301, 302 and 303, which drop the body, keep their current behavior.
- Plain string bodies already work and must keep working.
### Clarifying Questions
- Can one redirect chain contain several 307 hops, each of which must resend the same body?
- What should happen when the body is a stream that cannot be rewound, such as a generator or a pipe?
- Should 308 Permanent Redirect, which also preserves the method and the body, get the same treatment?
### What a Strong Answer Covers
- A causal chain from the timeout back to the root cause, including why the string body is unaffected
- Locating every place the body's state passes through between the first and the second send
- A fix that replays from the caller's original position, proven by the partial-read test
- Defined behavior for bodies that cannot be rewound, with redirects that drop the body left unchanged
- Running the three tests and the existing redirect tests to show that nothing regressed
### Follow-up Questions
- Why does this bug surface as a read timeout rather than as an error about the body?
- Would making the prepared request's copy deep-copy the stream fix the bug? What would it cost for a large file upload?
- How would you write a regression test for a stream whose `tell()` raises?
- How should a chunked body produced by a generator behave when the server answers 307?
Overview: A debugging exercise in the Python requests library: POST requests whose body is a seekable stream time out after an HTTP 307 redirect, while a plain string body works. It tests tracing how request state changes between the first send and the redirect, and fixing the library so that streams the caller has already partly read are resent correctly.
Read the full Stripe Software Engineer interview experience this question came from