Likely causes (framing / chunked-related)
- Missing/incorrect Content-Length or Transfer-Encoding headers so the client/server don’t agree on response boundary — server writes body but doesn’t signal end, next response bytes are treated as continuation.
- Incorrect implementation of chunked encoding: wrong chunk size lines, missing final 0\r\n\r\n, or forgetting CRLF between chunks.
- Mixing buffered and unbuffered I/O or shared buffers across connections/threads leading to interleaved writes on the same socket.
- HTTP parser/reader logic not keeping a strict request/response loop: reading more bytes into the next request or not draining request body before writing response, especially with pipelining.
- Keep-Alive + non-reentrant handlers: handler reuses state across requests on same connection.
Fixes
- Ensure deterministic framing:
- Always send either a correct Content-Length header or well-formed Transfer-Encoding: chunked (include chunk-size CRLF data CRLF, final 0\r\n\r\n).
- If using chunked, send the terminating 0\r\n\r\n reliably even on errors.
- Make request/response lifecycle strict:
- Read full request (including body) before writing response.
- Serialize writes per-connection; avoid global buffers. Use per-connection locks if multiple threads may write.
- Use battle-tested libraries (asyncio streams, h11, httptools, or Python’s http.server) rather than hand-rolled parsers.
- Optionally disable pipelining (send Connection: close or reject pipelined requests) if server cannot safely support it.
- Add instrumentation (bytes written, chunk counts, unexpected EOFs) and enable debug logs for framing.
Tests to add (regression + CI)
- Integration pipelining test: open a single TCP connection, send two or more valid requests back-to-back (without waiting), assert each response is exactly the intended body and headers, and that boundaries are respected.
- Chunked encoding correctness: send requests that trigger chunked responses; assert chunk sizes, CRLFs, and final 0 chunk present and responses parse correctly.
- Interleaving/concurrency test: multiple clients sending pipelined requests concurrently to same backend worker to surface shared-state races.
- Fuzz / property tests: randomize chunk sizes, include aborted connections, partial writes to ensure server always terminates chunked responses and doesn’t splice responses.
- End-to-end test with real HTTP parsers/clients (curl --http1.1 --no-buffer) to exercise real-world clients.
Example pytest integration test (basic)
python
import socket
def test_pipelined_responses():
s = socket.create_connection(('localhost', 8080))
req = ("GET /a HTTP/1.1\r\nHost: local\r\nConnection: keep-alive\r\n\r\n"
"GET /b HTTP/1.1\r\nHost: local\r\nConnection: close\r\n\r\n")
s.sendall(req.encode())
data = b''
while b'Content-Length' not in data:
data += s.recv(4096)
# naive split: ensure two distinct responses present and not concatenated bodies
assert b'HTTP/1.1 200' in data
assert data.count(b'HTTP/1.1 200') == 2
# further parse Content-Length headers and verify exact body lengths...
s.close()
Monitoring & alerts
- Add alerts for unexpected long-lived connections, unusually high bytes-per-connection, or increased parsing errors.
- Log framing errors with connection id + hex dump for post-incident debugging.
This combination of deterministic framing, using robust parsers, per-connection write isolation, and targeted tests will prevent concatenated responses from pipelined requests.