Fix multiple requests on same HTTP1 connection#78
Conversation
|
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 |
|
@guoye-zhang hm, interesting. I'll take a look at that. |
|
Just to but in here, this has fixed a load of test failures in Vapor that were failing with |
|
@guoye-zhang the failing conformance test now passes for me. |
|
I updated apple/swift-http-api-proposal#137 and it's still running into these errors with AHC conformance tests: |
|
|
|
I'm actually reproducing it 100% on my Mac running the test in Xcode. GitHub CI reproduced it as well. Previous workaround with |
# Conflicts: # Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift # Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift # Sources/NIOHTTPServer/NIOHTTPServer.swift
b96e87c to
4947a0d
Compare
This reverts commit 51d83be.
|
|
||
| case .body(let element): | ||
| nonisolated(unsafe) let iter = iterator | ||
| self.readerState.putIterator(iter) |
There was a problem hiding this comment.
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.
| // 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@FranzBusch I've implemented this change.
There was a problem hiding this comment.
may have already been sent back.
can we check, if they already have been sent back? if not add the connection: close header?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
We discussed this offline and decided that we would have a different approach: instead of the somewhat arbitrary 256kb payload drain, I've added an H1-only channel handler that adds the Connection:close header whenever we write a response .end or start sending back a response body when we haven't yet seen a request .end. After sending the request .end part, we'll close the connection.
| // Begin a new request. (Any previous request's response must have | ||
| // completed already since HTTPServerPipelineHandler enforces ordering.) |
There was a problem hiding this comment.
i think this is only true if pipelining is disabled.
There was a problem hiding this comment.
Hm, from the HTTPServerPipelineHandler docs:
This handler ensures that HTTP server pipelines only process one request at a time.
There was a problem hiding this comment.
yes, if we ensure that this is always enabled.
There was a problem hiding this comment.
That option is enabled so we currently have this handler set up, which means this should be safe. We don't provide a way to change this configuration.
| } | ||
| context.fireChannelRead(data) | ||
| } | ||
|
|
There was a problem hiding this comment.
I think this also needs a timeout in the form of channelReadComplete. We should check the state in channelReadComplete and flush the head.
There was a problem hiding this comment.
I'm trying to keep the timeouts in #72 - I haven't updated it since we had our chat though, I'll re-assign to you when I do.
There was a problem hiding this comment.
channelReadComplete isn't a timeout though :) It's another NIO callback.
There was a problem hiding this comment.
I know, but then I don't think I understand your original comment :D
| /// Informational (1xx) responses pass through unchanged and do not affect buffering | ||
| /// state. | ||
| @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) | ||
| final class HTTPKeepAliveHandler: ChannelDuplexHandler { |
There was a problem hiding this comment.
I really think we should unify the number of handlers here significantly. Doing this in a follow up is fine.
There was a problem hiding this comment.
Sure, but I don't want to do it at this point - the timeouts are in a separate PR, and having them separate makes reviewing easier IMO. I will consolidate them in a follow up.
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.