Skip to content

Fix multiple requests on same HTTP1 connection#78

Open
gjcairo wants to merge 10 commits intomainfrom
http1-fixes
Open

Fix multiple requests on same HTTP1 connection#78
gjcairo wants to merge 10 commits intomainfrom
http1-fixes

Conversation

@gjcairo
Copy link
Copy Markdown
Collaborator

@gjcairo gjcairo commented Apr 2, 2026

We're not currently handling keep alive HTTP1 connections properly: we close connections at the end of the first request, yet we don't send a connection close header. This PR fixes that by reusing the iterator until the stream is finished, so we can keep reading all requests that come through the same connection.

This also removes 6.2 support because there are some compiler issues with sendability that have not been properly addressed.

@gjcairo gjcairo added the 🆕 semver/minor Adds new public API. label Apr 2, 2026
Comment thread Sources/NIOHTTPServer/HTTPRequestConcludingAsyncReader.swift Outdated
@guoye-zhang
Copy link
Copy Markdown

I pointed apple/swift-http-api-proposal#137 to the latest commit on this branch, but the AHC conformance test is still failing it appears

@gjcairo
Copy link
Copy Markdown
Collaborator Author

gjcairo commented Apr 7, 2026

@guoye-zhang hm, interesting. I'll take a look at that.

@0xTim
Copy link
Copy Markdown
Member

0xTim commented Apr 9, 2026

Just to but in here, this has fixed a load of test failures in Vapor that were failing with Caught error: I/O on closed channel so thank you!

@gjcairo
Copy link
Copy Markdown
Collaborator Author

gjcairo commented Apr 13, 2026

@guoye-zhang the failing conformance test now passes for me.

@guoye-zhang
Copy link
Copy Markdown

I updated apple/swift-http-api-proposal#137 and it's still running into these errors with AHC conformance tests: ✘ Test testOk failed with error: I/O on closed channel

@gjcairo
Copy link
Copy Markdown
Collaborator Author

gjcairo commented Apr 14, 2026

testOk and testBasicRedirect are flaky, but they only fail like twice if I run them 100 times.
Are you seeing them fail more consistently than this? Are we sure this is an issue on the server side?

@guoye-zhang
Copy link
Copy Markdown

I'm actually reproducing it 100% on my Mac running the test in Xcode. GitHub CI reproduced it as well. Previous workaround with Connection: close never failed for me.

gjcairo added 7 commits April 24, 2026 11:04
# Conflicts:
#	Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
#	Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift
#	Sources/NIOHTTPServer/NIOHTTPServer.swift
@gjcairo gjcairo force-pushed the http1-fixes branch 2 times, most recently from b96e87c to 4947a0d Compare April 30, 2026 10:35

case .body(let element):
nonisolated(unsafe) let iter = iterator
self.readerState.putIterator(iter)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is unfortunate because we need to go via the lock after each read - may slow things down considerably. Not sure what the alternative is other than not put any of this behind a lock, which I believe should be okay since we shouldn't be consuming the reader from more than one place at a time.

Comment on lines +290 to +297
// If the handler didn't fully consume the request body, drain the remaining
// parts so the iterator is positioned at the start of the next request.
// Errors during draining are not propagated — if the drain fails, we simply
// cannot reuse this connection.
if !readerState.wrapped.withLock({ $0.finishedReading }) {
do {
drainLoop: while true {
switch try await recoveredIterator.next(isolation: #isolation) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We shouldn't do this. If the handler didn't fully consume the request body then we should close the connection in my opinion. Otherwise this could allow for attacks where an attacker crafts a request that leads to the handler responding early without having ready everything but keep the connection in an infinite draining state.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hm yeah okay, you're right about the security concern. But wouldn't it potentially be a valid use case for a handler to respond early without consuming the whole request? The conformance tests actually do this for some requests: they don't read the request body (and thus they don't read .end) because they only look at the headers (or even nothing). Are we saying we should document that all handler implementations should make sure they always fully consume the request?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I just took a quick look what Go does and they cap the drain to 256KB to stop bad actors. https://cs.opensource.google/go/go/+/master:src/net/http/server.go;l=1137?q=server.go&ss=go%2Fgo

@fabianfett WDYT?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think this is reasonable. The only thing is that we can't really send back Connection:close by the time we run this drain loop (which is what Go does), because a response (and thus headers) may have already been sent back. We would just close the connection - which maybe we're okay with bur just pointing it out.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@FranzBusch I've implemented this change.

Copy link
Copy Markdown
Member

@fabianfett fabianfett May 6, 2026

Choose a reason for hiding this comment

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

may have already been sent back.

can we check, if they already have been sent back? if not add the connection: close header?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

They will have been sent if the response sender was consumed, which happens whenever a Response is sent back to the client (by calling responseSender.send(...). It would be odd to implement a handler that sends nothing back to the client and just returns; but yes, we could check whether responseSender.send has been called if we think that's useful.

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.

6 participants