Skip to content

Allow NIOHTTPServer to use NIOAsyncTestingChannel for tests#39

Merged
aryan-25 merged 9 commits intoswift-server:mainfrom
aryan-25:async-testing-channel
Jan 21, 2026
Merged

Allow NIOHTTPServer to use NIOAsyncTestingChannel for tests#39
aryan-25 merged 9 commits intoswift-server:mainfrom
aryan-25:async-testing-channel

Conversation

@aryan-25
Copy link
Copy Markdown
Collaborator

@aryan-25 aryan-25 commented Jan 8, 2026

Motivation:

Currently, we perform end-to-end tests by starting NIOHTTPServer on localhost and setting up a HTTP client to send requests / observe responses. NIOHTTPServer internally uses ServerBootstrap to set up the server, but ServerBootstrap does not expose the child channels it creates for each connection, making NIOHTTPServer difficult to test closely.

We can improve this by refactoring NIOHTTPServer to also (in tests) allow a NIOAsyncTestingChannel to be used as the channel the server runs on, and manually feed/extract data from it to test different expectations.

Modifications:

  • Moved the serveInsecureHTTP1_1 and serveSecureUpgrade methods to new files (NIOHTTPServer+HTTP1_1.swift and NIOHTTPServer+SecureUpgrade.swift).
    • Split the server channel and child channel setup into different methods.
    • Added serveInsecureHTTP1_1WithTestChannel and serveSecureUpgradeWithTestChannel methods, which set up the input NIOAsyncTestingChannel as the channel the server runs on.
  • Added HTTP1ClientServerProvider and HTTPSecureUpgradeClientServerProvider, which can be used in tests to send requests / observe responses. These types currently have the following methods (this is very much open for discussion):
    • static func withProvider(handler:body:):
      • Sets up the server with the provided request handler closure, and vends a provider instance to the body closure.
    • func withConnectedClient(body:):
      • Sets up a connection and vends the client NIOAsyncChannel to the body closure which can be used to send requests / observe responses in terms of Swift HTTP types.
      • For Secure Upgrade, withConnectedClient vends a NegotiatedConnection enum, which, for HTTP/1.1 gives you the client NIOAsyncChannel, or for HTTP/2, a HTTP2StreamManager which has an openStream() method returning a client stream channel.
  • Added two example test-cases in NIOHTTPServerEndToEndTests.swift which use these new provider types.

Result:

Infrastructure in place to write end-to-end tests.

@aryan-25 aryan-25 marked this pull request as draft January 8, 2026 10:19
@aryan-25 aryan-25 marked this pull request as ready for review January 13, 2026 13:30
@gjcairo gjcairo added semver/none No version bump required. 🆕 semver/minor Adds new public API. and removed semver/none No version bump required. labels Jan 14, 2026
}
}

func serveInsecureHTTP1_1WithTestChannel(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we move this to some utility in the tests module?

// So, we create a client channel, and use it to send requests and observe responses in terms of HTTP types.
let (clientTestChannel, clientAsyncChannel) = try await self.setUpClientConnection()

try await withThrowingTaskGroup { group in
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this can be a throwingDiscardingTaskGroup, and we can get rid of the group.next() at the end.

Comment thread Sources/HTTPServer/NIOHTTPServer+HTTP1_1.swift
}
}

func serveSecureUpgradeWithTestChannel(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as for H1, can we move this to the test module?

try await outbound.write(.head(.init(method: .get, scheme: "", authority: "", path: "/")))
try await outbound.write(.end(nil))

outerLoop: for try await response in inbound {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we manually drive an iterator for inbound so we can assert that we're getting each of the parts in order too?

try await outbound.write(.head(.init(method: .get, scheme: "", authority: "", path: "/")))
try await outbound.write(.end(nil))

outerLoop: for try await response in inbound {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here as in the other test

@aryan-25 aryan-25 requested a review from gjcairo January 20, 2026 17:42
@aryan-25 aryan-25 merged commit 7acfd53 into swift-server:main Jan 21, 2026
19 of 26 checks passed
@aryan-25 aryan-25 deleted the async-testing-channel branch February 24, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants