Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test for HTTP1 request with large header #658

Merged
merged 8 commits into from
Jan 26, 2023

Conversation

dnadoba
Copy link
Collaborator

@dnadoba dnadoba commented Jan 25, 2023

Motivation

We currently don't handle large headers well which trigger a channel writability change event.

Modification

  • refactor HTTP1ConnectionHandler to make it more testable
  • Add failing (but currently skipped) tests which reproduces the issue

Result

We can reliably reproduce the large request header issue in an integration and unit test.
Note that the actual fix is not included to make reviewing easier and will come in a follow up PR.

@dnadoba dnadoba added the semver/patch For PRs that when merged will only cause a bump of the patch version, ie. 1.0.x -> 1.0.(x+1) label Jan 25, 2023
@@ -156,6 +157,7 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
metadata: req.requestFramingMetadata
)
self.run(action, context: context)
promise?.succeed(())
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't seem appropriate. We should presumably send the promise on with the appropriate data, rather than silently succeeding it here.

Copy link
Collaborator Author

@dnadoba dnadoba Jan 25, 2023

Choose a reason for hiding this comment

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

Good catch. I have reverted it now. I though I needed it because try channel.writeOutbound(request) would block indefinitely but I have now replaced it with channel.writeAndFlush(request, promise: nil) which doesn't block and is closer to the production code. efa86b5


init(connection: HTTP1Connection, eventLoop: EventLoop, logger: Logger) {
self.connection = connection
var onRequestCompleted: () -> Void = {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are we adding this indirection here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need it to break the dependency between HTTP1Connection and HTTP1ClientChannelHandler. HTTP1ClientChannelHandler only needs a reference to HTTP1Connection to get the connection id for logging and to signal that the last request is complete. The connection.id is now passed as connectionIdLoggerMetadata: Logger.MetadataValue in the init. connection.taskComplete() is replaced with onRequestCompleted() indirection.

I think in the long run we should replace this with a write promise we pass together with the request here:


This will cost us an additional allocation per request but this should be fine.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, this was my reaction: having unstructured callbacks like this is a bit of a worry for me. Promises are a substantial improvement.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there any reason not to make that change soon?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

HTTP2 currently uses the same state machine and doesn't need to store separate promise because it creates a new stream and therefore a new channel for each request. It uses the channel.closeFuture to detect that the request is done (well it will soon #657). So it doesn't need a separate promise but it also doesn't hurt either.

At the end nothing technically is blocking this, it's just work that need to be done.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, I'm asking only because I suspect this mystery-meat callback will live here forever if we don't make the change now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I have tried to do that. The problem I run into is that it is currently not really used like a promise and may never be called. It is used to signal that the connection is again able to receive further request which it may never be for various reasons. This case is currently handled by closing the channel instead of calling this callback. At the end it just calls down to connection.delegate.http1ConnectionReleased(connection)

I think changing and testing this is quite some effort which I would like not to do right now.

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 have at least renamed it to onConnectionIdle to better explain what it actually does: 5f5dc1e

@Lukasa Lukasa merged commit 67f99d1 into swift-server:main Jan 26, 2023
@dnadoba dnadoba deleted the dn-large-header-tests-http1 branch January 26, 2023 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/patch For PRs that when merged will only cause a bump of the patch version, ie. 1.0.x -> 1.0.(x+1)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants