diff --git a/Package.swift b/Package.swift index f4f588e0a..687b8ef77 100644 --- a/Package.swift +++ b/Package.swift @@ -21,18 +21,18 @@ let package = Package( .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.10.1"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"), .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"), ], targets: [ .target( name: "AsyncHTTPClient", - dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression"] + dependencies: ["NIO", "NIOHTTP1", "NIOSSL", "NIOConcurrencyHelpers", "NIOHTTPCompression", "NIOFoundationCompat"] ), .testTarget( name: "AsyncHTTPClientTests", - dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOSSL", "AsyncHTTPClient", "NIOFoundationCompat"] + dependencies: ["NIO", "NIOConcurrencyHelpers", "NIOSSL", "AsyncHTTPClient", "NIOFoundationCompat", "NIOTestUtils"] ), ] ) diff --git a/Sources/AsyncHTTPClient/HTTPHandler.swift b/Sources/AsyncHTTPClient/HTTPHandler.swift index 8e988b6b2..5747af0d3 100644 --- a/Sources/AsyncHTTPClient/HTTPHandler.swift +++ b/Sources/AsyncHTTPClient/HTTPHandler.swift @@ -15,6 +15,7 @@ import Foundation import NIO import NIOConcurrencyHelpers +import NIOFoundationCompat import NIOHTTP1 import NIOSSL @@ -758,11 +759,12 @@ extension TaskHandler: ChannelDuplexHandler { switch self.state { case .end: break - default: + case .body, .head, .idle, .redirected, .sent: self.state = .end let error = HTTPClientError.remoteConnectionClosed self.failTaskAndNotifyDelegate(error: error, self.delegate.didReceiveError) } + context.fireChannelInactive() } func errorCaught(context: ChannelHandlerContext, error: Error) { diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift index b0e0b6ad6..a2189df40 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift @@ -61,6 +61,11 @@ extension HTTPClientTests { ("testDecompressionLimit", testDecompressionLimit), ("testLoopDetectionRedirectLimit", testLoopDetectionRedirectLimit), ("testCountRedirectLimit", testCountRedirectLimit), + ("testWorksWith500Error", testWorksWith500Error), + ("testWorksWithHTTP10Response", testWorksWithHTTP10Response), + ("testWorksWhenServerClosesConnectionAfterReceivingRequest", testWorksWhenServerClosesConnectionAfterReceivingRequest), + ("testSubsequentRequestsWorkWithServerSendingConnectionClose", testSubsequentRequestsWorkWithServerSendingConnectionClose), + ("testSubsequentRequestsWorkWithServerAlternatingBetweenKeepAliveAndClose", testSubsequentRequestsWorkWithServerAlternatingBetweenKeepAliveAndClose), ] } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 68dfcc82c..b7e9aa382 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -18,11 +18,25 @@ import NIOFoundationCompat import NIOHTTP1 import NIOHTTPCompression import NIOSSL +import NIOTestUtils import XCTest class HTTPClientTests: XCTestCase { typealias Request = HTTPClient.Request + var group: EventLoopGroup! + + override func setUp() { + XCTAssertNil(self.group) + self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + } + + override func tearDown() { + XCTAssertNotNil(self.group) + XCTAssertNoThrow(try self.group.syncShutdownGracefully()) + self.group = nil + } + func testRequestURI() throws { let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar") XCTAssertEqual(request1.url.host, "someserver.com") @@ -658,4 +672,172 @@ class HTTPClientTests: XCTestCase { XCTAssertEqual(error as! HTTPClientError, HTTPClientError.redirectLimitReached) } } + + func testWorksWith500Error() { + let web = NIOHTTP1TestServer(group: self.group) + defer { + XCTAssertNoThrow(try web.stop()) + } + + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + } + let result = httpClient.get(url: "http://localhost:\(web.serverPort)/foo") + + XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1), + method: .GET, + uri: "/foo", + headers: HTTPHeaders([("Host", "localhost"), + // The following line can be removed once we + // have a connection pool. + ("Connection", "close"), + ("Content-Length", "0")]))), + try web.readInbound())) + XCTAssertNoThrow(XCTAssertEqual(.end(nil), + try web.readInbound())) + XCTAssertNoThrow(try web.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), + status: .internalServerError)))) + XCTAssertNoThrow(try web.writeOutbound(.end(nil))) + + var response: HTTPClient.Response? + XCTAssertNoThrow(response = try result.wait()) + XCTAssertEqual(.internalServerError, response?.status) + XCTAssertNil(response?.body) + } + + func testWorksWithHTTP10Response() { + let web = NIOHTTP1TestServer(group: self.group) + defer { + XCTAssertNoThrow(try web.stop()) + } + + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + } + let result = httpClient.get(url: "http://localhost:\(web.serverPort)/foo") + + XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1), + method: .GET, + uri: "/foo", + headers: HTTPHeaders([("Host", "localhost"), + // The following line can be removed once we + // have a connection pool. + ("Connection", "close"), + ("Content-Length", "0")]))), + try web.readInbound())) + XCTAssertNoThrow(XCTAssertEqual(.end(nil), + try web.readInbound())) + XCTAssertNoThrow(try web.writeOutbound(.head(.init(version: .init(major: 1, minor: 0), + status: .internalServerError)))) + XCTAssertNoThrow(try web.writeOutbound(.end(nil))) + + var response: HTTPClient.Response? + XCTAssertNoThrow(response = try result.wait()) + XCTAssertEqual(.internalServerError, response?.status) + XCTAssertNil(response?.body) + } + + func testWorksWhenServerClosesConnectionAfterReceivingRequest() { + let web = NIOHTTP1TestServer(group: self.group) + + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + } + let result = httpClient.get(url: "http://localhost:\(web.serverPort)/foo") + + XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1), + method: .GET, + uri: "/foo", + headers: HTTPHeaders([("Host", "localhost"), + // The following line can be removed once we + // have a connection pool. + ("Connection", "close"), + ("Content-Length", "0")]))), + try web.readInbound())) + XCTAssertNoThrow(XCTAssertEqual(.end(nil), + try web.readInbound())) + XCTAssertNoThrow(try web.stop()) + + XCTAssertThrowsError(try result.wait()) { error in + XCTAssertEqual(HTTPClientError.remoteConnectionClosed, error as? HTTPClientError) + } + } + + func testSubsequentRequestsWorkWithServerSendingConnectionClose() { + let web = NIOHTTP1TestServer(group: self.group) + defer { + XCTAssertNoThrow(try web.stop()) + } + + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + } + + for _ in 0..<10 { + let result = httpClient.get(url: "http://localhost:\(web.serverPort)/foo") + + XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1), + method: .GET, + uri: "/foo", + headers: HTTPHeaders([("Host", "localhost"), + // The following line can be removed once + // we have a connection pool. + ("Connection", "close"), + ("Content-Length", "0")]))), + try web.readInbound())) + XCTAssertNoThrow(XCTAssertEqual(.end(nil), + try web.readInbound())) + XCTAssertNoThrow(try web.writeOutbound(.head(.init(version: .init(major: 1, minor: 0), + status: .ok, + headers: HTTPHeaders([("connection", "close")]))))) + XCTAssertNoThrow(try web.writeOutbound(.end(nil))) + + var response: HTTPClient.Response? + XCTAssertNoThrow(response = try result.wait()) + XCTAssertEqual(.ok, response?.status) + XCTAssertNil(response?.body) + } + } + + func testSubsequentRequestsWorkWithServerAlternatingBetweenKeepAliveAndClose() { + let web = NIOHTTP1TestServer(group: self.group) + defer { + XCTAssertNoThrow(try web.stop()) + } + + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.group)) + defer { + XCTAssertNoThrow(try httpClient.syncShutdown()) + } + + for i in 0..<10 { + let result = httpClient.get(url: "http://localhost:\(web.serverPort)/foo") + + XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1), + method: .GET, + uri: "/foo", + headers: HTTPHeaders([("Host", "localhost"), + // The following line can be removed once + // we have a connection pool. + ("Connection", "close"), + ("Content-Length", "0")]))), + try web.readInbound())) + XCTAssertNoThrow(XCTAssertEqual(.end(nil), + try web.readInbound())) + XCTAssertNoThrow(try web.writeOutbound(.head(.init(version: .init(major: 1, minor: 0), + status: .ok, + headers: HTTPHeaders([("connection", + i % 2 == 0 ? "close" : "keep-alive")]))))) + XCTAssertNoThrow(try web.writeOutbound(.end(nil))) + + var response: HTTPClient.Response? + XCTAssertNoThrow(response = try result.wait()) + XCTAssertEqual(.ok, response?.status) + XCTAssertNil(response?.body) + } + } }