Skip to content
New issue

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

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

Already on GitHub? # to your account

Fix up HTTP message framing edge cases. #298

Merged
merged 2 commits into from
Apr 12, 2018

Conversation

Lukasa
Copy link
Contributor

@Lukasa Lukasa commented Apr 10, 2018

Motivation:

HTTP message framing has a number of edge cases that NIO currently does
not tolerate. We should decide what our position is on each of these edge
cases and handle it appropriately.

Modifications:

Provide an extensive test suite that codifies our expected handling of
these edge cases. Fix divergences from this behaviour.

Result:

Better tolerance for the weird corners of HTTP.

Resolves #254.

@Lukasa Lukasa added the 🔨 semver/patch No public API change. label Apr 10, 2018
@Lukasa Lukasa requested review from normanmaurer and weissi April 10, 2018 17:20
@Lukasa
Copy link
Contributor Author

Lukasa commented Apr 10, 2018

Sorry about the size of this patch, but FWIW the vast, vast majority of it is a series of tests to clarify our handling of the various edge cases.


if ((head.status.code / 100 == 1) || // 1XX codes
(head.status.code == 204) ||
(head.status.code == 304)) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: You don't need the (...) around rich of these

}

// We don't need the cumulation buffer, if we're holding it.
self.cumulationBuffer = nil
Copy link
Member

Choose a reason for hiding this comment

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

@Lukasa should we set this to nil before we call channelReadComplete to reduce the reference count and so minimize the chances of CoW ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, in fact we can set it to nil very early in this function without danger.

case .body:
self.seenBody = true
case .end:
self.seenEnd = true
Copy link
Member

Choose a reason for hiding this comment

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

should we also assert that we not seen the same state before ?

XCTAssertTrue(handler.receivedEnd)
XCTAssertTrue(handler.eof)

XCTAssertNoThrow(try channel.finish())
Copy link
Member

Choose a reason for hiding this comment

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

I think we should actually assert the return value of channel.finish()

XCTAssertFalse(handler.seenBody)
XCTAssert(handler.seenEnd)

XCTAssertNoThrow(try channel.finish())
Copy link
Member

Choose a reason for hiding this comment

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

I think we should actually assert the return value of channel.finish()

XCTAssertFalse(handler.seenBody)
XCTAssert(handler.seenEnd)

XCTAssertNoThrow(try channel.finish())
Copy link
Member

Choose a reason for hiding this comment

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

I think we should actually assert the return value of channel.finish()

}

// Must spin the loop.
(channel.eventLoop as! EmbeddedEventLoop).run()
Copy link
Member

Choose a reason for hiding this comment

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

Assert that the channel is not active anymore

}

// Must spin the loop.
(channel.eventLoop as! EmbeddedEventLoop).run()
Copy link
Member

Choose a reason for hiding this comment

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

assert the channel is not native anymore.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should actually assert the return value of channel.finish()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, we can't do that here because channel.finish() will throw if the channel is already closed.

}

// Must spin the loop.
(channel.eventLoop as! EmbeddedEventLoop).run()
Copy link
Member

Choose a reason for hiding this comment

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

assert the channel is not active anymore

Copy link
Member

Choose a reason for hiding this comment

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

I think we should actually assert the return value of channel.finish()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, we can't do that here because channel.finish() will throw if the channel is already closed.

XCTAssertFalse(handler.seenBody)
XCTAssert(handler.seenEnd)

XCTAssertNoThrow(try channel.finish())
Copy link
Member

Choose a reason for hiding this comment

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

assert the return value

}

// Must spin the loop.
(channel.eventLoop as! EmbeddedEventLoop).run()
Copy link
Member

Choose a reason for hiding this comment

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

assert that the channel is not active anymore and also call channel.finish() and assert return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, we can't do that here because channel.finish() will throw if the channel is already closed.

@Lukasa Lukasa force-pushed the cb-http-semantic-eof branch from c8df6c6 to 9364f0c Compare April 11, 2018 13:15
@Lukasa
Copy link
Contributor Author

Lukasa commented Apr 11, 2018

Ok @normanmaurer, please re-review.


/// Tests for the HTTP decoder's handling of message body framing.
///
/// Mostly tests assertions in RFC 7230 § 3.3.3.
Copy link
Member

Choose a reason for hiding this comment

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

@Lukasa nit: consider linking the rfc directly via markdown. (feel free to ignore tho)

XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .init(major: 1, minor: 1),
method: requestMethod,
uri: "/"))))
_ = channel.readOutbound()
Copy link
Member

Choose a reason for hiding this comment

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

should we ensure this not returns nil ?

Copy link
Member

@normanmaurer normanmaurer left a comment

Choose a reason for hiding this comment

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

Just one thing... LGTM otherwise

XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/"))))
_ = channel.readOutbound()
Copy link
Member

Choose a reason for hiding this comment

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

should we ensure this not return nil ?

@Lukasa Lukasa force-pushed the cb-http-semantic-eof branch from 9364f0c to e3c3bd9 Compare April 11, 2018 13:38
@Lukasa
Copy link
Contributor Author

Lukasa commented Apr 11, 2018

Ok, addressed.

Copy link
Member

@normanmaurer normanmaurer left a comment

Choose a reason for hiding this comment

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

Nice work!

@Lukasa
Copy link
Contributor Author

Lukasa commented Apr 11, 2018

@swift-nio-bot test this please

1 similar comment
@tomerd
Copy link
Member

tomerd commented Apr 11, 2018

@swift-nio-bot test this please

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

thanks so much, this is amazing!!! I left two comments but still approved it but it'd be awesome if you could rename those errno variables.

// No check to state.currentError becuase, if we hit it before, we already threw that
// error. This never calls any of the callbacks that set that field anyway. Instead we
// just check if the errno is set and throw.
let errno = parser.http_errno
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a huge fan of naming variables errno because if you rename them and forget the usage sites, it'll still work. Mind making it httpError or httpErrno or something?

@@ -395,7 +416,8 @@ public class HTTPDecoder<HTTPMessageT>: ByteToMessageDecoder, AnyHTTPDecoder {

let errno = parser.http_errno
if errno != 0 {
throw HTTPParserError.httpError(fromCHTTPParserErrno: http_errno(rawValue: errno))!
self.state.currentError = HTTPParserError.httpError(fromCHTTPParserErrno: http_errno(rawValue: errno))!
Copy link
Member

Choose a reason for hiding this comment

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

whilst we're on it, mind also renaming that errno variable up there?

@weissi
Copy link
Member

weissi commented Apr 12, 2018

@Lukasa compilation failed though:

19:01:45 Linking ./.build/x86_64-unknown-linux/debug/NIOWebSocketServer
19:01:55 /code/Tests/NIOHTTP1Tests/HTTPDecoderLengthTest.swift:155:32: error: value of optional type '[UInt8]?' not unwrapped; did you mean to use '!' or '?'?
19:01:55         XCTAssertEqual(handler.body, Array("some body data".utf8))
19:01:55                                ^
19:01:55                                    !
19:01:55 /code/Tests/NIOHTTP1Tests/HTTPDecoderLengthTest.swift:166:32: error: value of optional type '[UInt8]?' not unwrapped; did you mean to use '!' or '?'?
19:01:55         XCTAssertEqual(handler.body, Array("some body data".utf8))
19:01:55                                ^
19:01:55                                    !

Motivation:

HTTP message framing has a number of edge cases that NIO currently does
not tolerate. We should decide what our position is on each of these edge
cases and handle it appropriately.

Modifications:

Provide an extensive test suite that codifies our expected handling of
these edge cases. Fix divergences from this behaviour.

Result:

Better tolerance for the weird corners of HTTP.
@Lukasa Lukasa force-pushed the cb-http-semantic-eof branch from e36e55b to 70bd13e Compare April 12, 2018 11:58
@Lukasa
Copy link
Contributor Author

Lukasa commented Apr 12, 2018

@jw Ok, that should be addressed.

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

thanks, approved!

@normanmaurer
Copy link
Member

@swift-nio-bot test this please

@Lukasa Lukasa added this to the 1.5.0 milestone Apr 12, 2018
@Lukasa Lukasa merged commit bc6e3db into apple:master Apr 12, 2018
@Lukasa Lukasa deleted the cb-http-semantic-eof branch April 12, 2018 14:12
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants