Skip to content

Buffer HTTP response bytes in HTTPClient - #245

Merged
swhitty merged 1 commit into
swhitty:mainfrom
PADL:client-buffered-read
Oct 4, 2026
Merged

swhitty merged 1 commit into
swhitty:mainfrom
PADL:client-buffered-read

Conversation

@lhoward

@lhoward lhoward commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

HTTPConnection wraps socket.bytes in AsyncBufferingSequence so that the decoder does not pull one byte per syscall while it parses the request line and headers (6238522). HTTPClient was left reading the response unbuffered: AsyncSocket.readResponse() hands the raw bytes to decodeResponse, so the status line and every header are read through iterator.next(), one read(2) each.

This shows up for a client that makes many small requests. In a perf profile of a daemon registering about 260 resources with an HTTP registry at start-up, the client's response reading was around 0.5 s of CPU on a Cortex-A72, most of it in the single-byte AsyncSocket.read() and CollectUntil.

Change

HTTPClient.sendHTTPRequest opens a socket for each request and reads one response from it, so it now reads that response through an AsyncBufferingSequence. Nothing reads from the socket afterwards, so nothing is lost to the buffer.

AsyncSocket.readResponse() itself stays unbuffered. Its other callers (the tests) go on to read WebSocket frames or further responses from the same socket, and bytes pulled past the response would not reach them. It gains a readResponse(from:) taking the bytes to read.

Measurements

Release build on x86-64 Linux, 300 requests to a loopback HTTPServer whose response has six headers and a 600 byte body:

per request before after
process CPU (client and server together) 0.43–0.60 ms 0.35 ms
wall time 0.29–0.38 ms 0.22 ms

Testing

  • New test: the client receives headers and a 20,000 byte body, larger than the 4 KB buffer, intact. It sits with the existing client test, which is Darwin-only; I also ran both client tests on Linux with the guard lifted, and they pass.
  • swift test on Linux (Swift 6.4): 300 tests, the same 3 issues as on main without this change (HTTPDecoderTests.standardizedPath and naughtyPath_IsParsed, both about /../ path standardisation).
  • Not run on Darwin locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p

HTTPConnection wraps socket.bytes in AsyncBufferingSequence so that the
decoder does not pull one byte per syscall while it parses the request
line and headers (6238522). HTTPClient was left reading the response
unbuffered: `AsyncSocket.readResponse()` hands the raw bytes to
`decodeResponse`, so the status line and every header are read through
`iterator.next()`, one read(2) each.

HTTPClient opens a socket for each request and reads one response from
it, so it can read through a buffer without anything being lost to a
later reader. `AsyncSocket.readResponse()` itself stays unbuffered: its
callers go on to read WebSocket frames or further responses from the
same socket, and bytes pulled past the response would not reach them.

Measurements: release build on x86-64 Linux, 300 requests to a loopback
HTTPServer whose response has six headers and a 600 byte body. Process
CPU per request, client and server together, 0.43-0.60 ms -> 0.35 ms;
wall time per request 0.29-0.38 ms -> 0.22 ms.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8cahEd4GfsHQ89sNrmP4p
@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.70%. Comparing base (bd32d43) to head (651ae1e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #245      +/-   ##
==========================================
- Coverage   93.72%   93.70%   -0.02%     
==========================================
  Files          72       72              
  Lines        3777     3783       +6     
==========================================
+ Hits         3540     3545       +5     
- Misses        237      238       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@swhitty
swhitty merged commit 4d2d822 into swhitty:main Oct 4, 2026
7 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants