HTTPEncoder: fix 0 length chunks (#1524)

Motivation:

Previously, if the user sent us a 0 length chunk, we would encode it as
`0\r\n\r\n` which means the _end_ of the body in chunked encoding. But
the end in NIO is send by sending `.end(...)`.

Modifications:

Don't write anything for 0 length writes.

Result:

More correct behaviour with 0 length body chunks.
This commit is contained in:
Johannes Weiss 2020-05-18 18:50:43 +01:00 committed by GitHub
parent b174051b81
commit c1d86289ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 134 additions and 1 deletions

View File

@ -143,13 +143,18 @@ public final class HTTPRequestEncoder: ChannelOutboundHandler, RemovableChannelH
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
switch self.unwrapOutboundIn(data) {
case .head(var request):
self.isChunked = sanitizeTransportHeaders(hasBody: request.method.hasRequestBody, headers: &request.headers, version: request.version) == .chunked
writeHead(wrapOutboundOut: self.wrapOutboundOut, writeStartLine: { buffer in
buffer.write(request: request)
}, context: context, headers: request.headers, promise: promise)
case .body(let bodyPart):
guard bodyPart.readableBytes > 0 else {
// Empty writes shouldn't send any bytes in chunked or identity encoding.
context.write(self.wrapOutboundOut(bodyPart), promise: promise)
return
}
writeChunk(wrapOutboundOut: self.wrapOutboundOut, context: context, isChunked: self.isChunked, chunk: bodyPart, promise: promise)
case .end(let trailers):
writeTrailers(wrapOutboundOut: self.wrapOutboundOut, context: context, isChunked: self.isChunked, trailers: trailers, promise: promise)

View File

@ -37,6 +37,10 @@ extension HTTPRequestEncoderTests {
("testNoChunkedEncodingForHTTP10", testNoChunkedEncodingForHTTP10),
("testBody", testBody),
("testCONNECT", testCONNECT),
("testChunkedEncodingIsTheDefault", testChunkedEncodingIsTheDefault),
("testChunkedEncodingCanBetEnabled", testChunkedEncodingCanBetEnabled),
("testChunkedEncodingDealsWithZeroLengthChunks", testChunkedEncodingDealsWithZeroLengthChunks),
("testChunkedEncodingWorksIfNoPromisesAreAttachedToTheWrites", testChunkedEncodingWorksIfNoPromisesAreAttachedToTheWrites),
]
}
}

View File

@ -140,6 +140,130 @@ class HTTPRequestEncoderTests: XCTestCase {
assertOutboundContainsOnly(channel, "")
}
func testChunkedEncodingIsTheDefault() {
let channel = EmbeddedChannel(handler: HTTPRequestEncoder())
var buffer = channel.allocator.buffer(capacity: 16)
var expected = channel.allocator.buffer(capacity: 32)
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/"))))
expected.writeString("POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
buffer.writeString("foo")
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.body(.byteBuffer(buffer))))
expected.clear()
expected.writeString("3\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("foo")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("0\r\n\r\n")
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(nil)))
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
}
func testChunkedEncodingCanBetEnabled() {
let channel = EmbeddedChannel(handler: HTTPRequestEncoder())
var buffer = channel.allocator.buffer(capacity: 16)
var expected = channel.allocator.buffer(capacity: 32)
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/",
headers: ["TrAnSfEr-encoding": "chuNKED"]))))
expected.writeString("POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
buffer.writeString("foo")
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.body(.byteBuffer(buffer))))
expected.clear()
expected.writeString("3\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("foo")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("0\r\n\r\n")
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(nil)))
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
}
func testChunkedEncodingDealsWithZeroLengthChunks() {
let channel = EmbeddedChannel(handler: HTTPRequestEncoder())
var buffer = channel.allocator.buffer(capacity: 16)
var expected = channel.allocator.buffer(capacity: 32)
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/"))))
expected.writeString("POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
buffer.clear()
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.body(.byteBuffer(buffer))))
XCTAssertNoThrow(XCTAssertEqual(0, try channel.readOutbound(as: ByteBuffer.self)?.readableBytes))
XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(["foo": "bar"])))
expected.clear()
expected.writeString("0\r\nfoo: bar\r\n\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
}
func testChunkedEncodingWorksIfNoPromisesAreAttachedToTheWrites() {
let channel = EmbeddedChannel(handler: HTTPRequestEncoder())
var buffer = channel.allocator.buffer(capacity: 16)
var expected = channel.allocator.buffer(capacity: 32)
channel.write(HTTPClientRequestPart.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/")), promise: nil)
channel.flush()
expected.writeString("POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
buffer.writeString("foo")
channel.write(HTTPClientRequestPart.body(.byteBuffer(buffer)), promise: nil)
channel.flush()
expected.clear()
expected.writeString("3\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("foo")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("\r\n")
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
expected.clear()
expected.writeString("0\r\n\r\n")
channel.write(HTTPClientRequestPart.end(nil), promise: nil)
channel.flush()
XCTAssertNoThrow(XCTAssertEqual(expected, try channel.readOutbound(as: ByteBuffer.self)))
XCTAssertNoThrow(XCTAssertTrue(try channel.finish().isClean))
}
private func assertOutboundContainsOnly(_ channel: EmbeddedChannel, _ expected: String) {
XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self).map { buffer in
buffer.assertContainsOnly(expected)