Skip to content

Support transparent decompression with HTTP/2 #610

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

Merged
merged 1 commit into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Logging
import NIOCore
import NIOHTTP2
import NIOHTTPCompression

protocol HTTP2ConnectionDelegate {
func http2Connection(_: HTTP2Connection, newMaxStreamSetting: Int)
Expand Down Expand Up @@ -79,17 +80,20 @@ final class HTTP2Connection {
/// request.
private var openStreams = Set<ChannelBox>()
let id: HTTPConnectionPool.Connection.ID
let decompression: HTTPClient.Decompression

var closeFuture: EventLoopFuture<Void> {
self.channel.closeFuture
}

init(channel: Channel,
connectionID: HTTPConnectionPool.Connection.ID,
decompression: HTTPClient.Decompression,
delegate: HTTP2ConnectionDelegate,
logger: Logger) {
self.channel = channel
self.id = connectionID
self.decompression = decompression
self.logger = logger
self.multiplexer = HTTP2StreamMultiplexer(
mode: .client,
Expand Down Expand Up @@ -118,7 +122,7 @@ final class HTTP2Connection {
configuration: HTTPClient.Configuration,
logger: Logger
) -> EventLoopFuture<(HTTP2Connection, Int)> {
let connection = HTTP2Connection(channel: channel, connectionID: connectionID, delegate: delegate, logger: logger)
let connection = HTTP2Connection(channel: channel, connectionID: connectionID, decompression: configuration.decompression, delegate: delegate, logger: logger)
return connection.start().map { maxStreams in (connection, maxStreams) }
}

Expand Down Expand Up @@ -208,9 +212,14 @@ final class HTTP2Connection {
// We only support http/2 over an https connection – using the Application-Layer
// Protocol Negotiation (ALPN). For this reason it is safe to fix this to `.https`.
let translate = HTTP2FramePayloadToHTTP1ClientCodec(httpProtocol: .https)
let handler = HTTP2ClientRequestHandler(eventLoop: channel.eventLoop)

try channel.pipeline.syncOperations.addHandler(translate)

if case .enabled(let limit) = self.decompression {
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
try channel.pipeline.syncOperations.addHandler(decompressHandler)
}

let handler = HTTP2ClientRequestHandler(eventLoop: channel.eventLoop)
try channel.pipeline.syncOperations.addHandler(handler)

// We must add the new channel to the list of open channels BEFORE we write the
Expand Down
3 changes: 3 additions & 0 deletions Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,9 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
let sync = channel.pipeline.syncOperations

try sync.addHandler(HTTP2FramePayloadToHTTP1ServerCodec())
if self.mode.compress {
try sync.addHandler(HTTPResponseCompressor())
}
try sync.addHandler(self.handlerFactory(connectionID))

return channel.eventLoop.makeSucceededVoidFuture()
Expand Down
1 change: 1 addition & 0 deletions Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ extension HTTPClientTests {
("testUploadStreaming", testUploadStreaming),
("testEventLoopArgument", testEventLoopArgument),
("testDecompression", testDecompression),
("testDecompressionHTTP2", testDecompressionHTTP2),
("testDecompressionLimit", testDecompressionLimit),
("testLoopDetectionRedirectLimit", testLoopDetectionRedirectLimit),
("testCountRedirectLimit", testCountRedirectLimit),
Expand Down
43 changes: 43 additions & 0 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,49 @@ class HTTPClientTests: XCTestCase {
}
}

func testDecompressionHTTP2() throws {
let localHTTPBin = HTTPBin(.http2(compress: true))
let localClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(
certificateVerification: .none,
decompression: .enabled(limit: .none)
)
)

defer {
XCTAssertNoThrow(try localClient.syncShutdown())
XCTAssertNoThrow(try localHTTPBin.shutdown())
}

var body = ""
for _ in 1...1000 {
body += "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
}

for algorithm: String? in [nil] {
var request = try HTTPClient.Request(url: "https://localhost:\(localHTTPBin.port)/post", method: .POST)
request.body = .string(body)
if let algorithm = algorithm {
request.headers.add(name: "Accept-Encoding", value: algorithm)
}

let response = try localClient.execute(request: request).wait()
var responseBody = try XCTUnwrap(response.body)
let data = try responseBody.readJSONDecodable(RequestInfo.self, length: responseBody.readableBytes)

XCTAssertEqual(.ok, response.status)
let contentLength = try XCTUnwrap(response.headers["Content-Length"].first.flatMap { Int($0) })
XCTAssertGreaterThan(body.count, contentLength)
if let algorithm = algorithm {
XCTAssertEqual(algorithm, response.headers["Content-Encoding"].first)
} else {
XCTAssertEqual("deflate", response.headers["Content-Encoding"].first)
}
XCTAssertEqual(body, data?.data)
}
}

func testDecompressionLimit() throws {
let localHTTPBin = HTTPBin(.http1_1(compress: true))
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup), configuration: .init(decompression: .enabled(limit: .ratio(1))))
Expand Down