Skip to content

Add new large body conformance tests#114

Merged
xbhatnag merged 5 commits intomainfrom
large-body
Feb 26, 2026
Merged

Add new large body conformance tests#114
xbhatnag merged 5 commits intomainfrom
large-body

Conversation

@xbhatnag
Copy link
Collaborator

Motivation

For our definition of a large body (1Mb), ensure that clients can:

  • make requests to a server with large body
  • receive responses from a server with large body
  • under-read a response from a server with large body

Modifications

Wrote 2 conformance tests:

  • test1MbBody writes a 1Mb body in a request to the /echo endpoint. The server echos the response back and the client verifies that it got back the exact same data.
  • testUnderRead makes a request to the new /1mb_body endpoint and only reads 1 byte in the response handler.

Unfortunately, testUnderRead did not pass on URLSession or AHC.

On AHC, attempting to under-read causes a precondition assertion failure here which needs more investigation.

For URLSession, the bug is that an under-read puts the state machine into a situation where:

  • the URLSession data task has been suspended
  • the post response callback handler hangs on the callback stream because the URLSession data task has been suspended.

A fix for this bug in URLSession is also included in this PR. The solution is to cancel the URLSessionTask once the response handler has executed. If the request is not already complete, this will cause the completeWithError handler to be triggered which will unblock the callback stream.

Result

These tests both pass with the URLSession client. test1MbBody passes with AHC and we will investigate why under-read is failing on AHC.

Test Plan

The raw requests and responses were verified manually with Wireshark.

These tests verify that a client can send a 1Mb request body to a server and receive a 1Mb response body from a server.
This test reads only 1 byte out of a 1Mb response body from the server. The "user" does not care about the rest of the body.
This caused a hang in URLSession and a precondition crash in AHC.
The fix for URLSession is also in this commit: After running the response handler, the "after response" delegate callback processor should close the callback stream and stop accepting new callbacks.
Otherwise, the stream will never be finished in this case because the URLSession data task has been suspended, due to receiving more data than the high watermark.
…large-body

# Conflicts:
#	Sources/HTTPClientConformance/HTTPClientConformance.swift
Also let the server gracefully fail to write the 1Mb body, because the client closed the socket early.
)

// Read only a single byte from the body. We do not care about the rest of the 1Mb.
try await client.perform(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this throw? I'm wondering if we should always throw if the entire response body wasn't read

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it doesn't throw for reading less. I don't think it should? It's valid for a client to just want to read an initial portion of the response before moving on. Real-world examples would be things like:

  • Content probing (media players reading only the initial bit of the file to get metadata)
  • Search/grep in large remote files

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about truncation issues where people only call read once, and usually get the entire body when it's small, but the code fails randomly when the body is bigger. Cancellation throws an error and this is a cancellation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see how that can happen.

Here are my concerns:

  1. If we're talking about CancellationError specifically, that is strictly about a task being cancelled. There was no task that was cancelled here explicitly by the user.
  2. Throwing an error in this case can encourage a pattern where the user wraps their call in a do-catch that does nothing in the catch, because they intended to stop halfway. That is awkward IMO.
  3. From the "socket-is-a-file" philosophy, there isn't a file API I know of that throws an error because we closed the file halfway through and didn't read it entirely, even if the close is implicit like in Python:
with open("myfile.txt") as f:
    f.read(1024)

That being said, python also makes it convenient to read "everything" by just omitting the size parameter. Maybe what we want here is some convenience functions to read until EOF, so users don't accidentally reach for the POSIX-y socket best-effort read?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect is supposed to be that. Honestly I think this is somewhat a bug in collect because it's returning nil as trailer which might be incorrect since we wouldn't have read the trailers before the end of the body. This is somewhat related to #52

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In URLSession at least, you have to cancel the task and receive a -999 error, otherwise we would keep delivering the body.

@xbhatnag xbhatnag added the 🔨 semver/patch No public API change. label Feb 25, 2026
@xbhatnag xbhatnag merged commit 85ea3ee into main Feb 26, 2026
18 of 22 checks passed
@xbhatnag xbhatnag deleted the large-body branch February 26, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants