Adopt GitHub actions (#780)

Migrate CI to use GitHub Actions.

### Motivation:

To migrate to GitHub actions and centralised infrastructure.

### Modifications:

Changes of note:
* Adopt swift-format using rules from SwiftNIO.
* Remove scripts and docker files which are no longer needed.
* Disabled warnings-as-errors on Swift 6.0 CI pipelines for now.

### Result:

Feature parity with old CI.
This commit is contained in:
Rick Newton-Rogers
2024-10-29 15:01:46 +00:00
committed by GitHub
parent acaca2d50d
commit c621142327
123 changed files with 7229 additions and 4394 deletions
+18
View File
@@ -0,0 +1,18 @@
name: Main
on:
push:
branches: [main]
schedule:
- cron: "0 8,20 * * *"
jobs:
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
+30
View File
@@ -0,0 +1,30 @@
name: PR
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
soundness:
name: Soundness
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
with:
license_header_check_project_name: "AsyncHTTPClient"
unit-tests:
name: Unit tests
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
with:
linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error"
linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
cxx-interop:
name: Cxx interop
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
swift-6-language-mode:
name: Swift 6 Language Mode
uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main
if: false # Disabled for now.
+18
View File
@@ -0,0 +1,18 @@
name: PR label
on:
pull_request:
types: [labeled, unlabeled, opened, reopened, synchronize]
jobs:
semver-label-check:
name: Semantic Version label check
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Check for Semantic Version label
uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main
+37
View File
@@ -0,0 +1,37 @@
.gitignore
**/.gitignore
.licenseignore
.gitattributes
.git-blame-ignore-revs
.mailfilter
.mailmap
.spi.yml
.swift-format
.editorconfig
.github/*
*.md
*.txt
*.yml
*.yaml
*.json
Package.swift
**/Package.swift
Package@-*.swift
**/Package@-*.swift
Package.resolved
**/Package.resolved
Makefile
*.modulemap
**/*.modulemap
**/*.docc/*
*.xcprivacy
**/*.xcprivacy
*.symlink
**/*.symlink
Dockerfile
**/Dockerfile
.dockerignore
Snippets/*
dev/git.commit.template
.unacceptablelanguageignore
Tests/AsyncHTTPClientTests/Resources/*.pem
+68
View File
@@ -0,0 +1,68 @@
{
"version" : 1,
"indentation" : {
"spaces" : 4
},
"tabWidth" : 4,
"fileScopedDeclarationPrivacy" : {
"accessLevel" : "private"
},
"spacesAroundRangeFormationOperators" : false,
"indentConditionalCompilationBlocks" : false,
"indentSwitchCaseLabels" : false,
"lineBreakAroundMultilineExpressionChainComponents" : false,
"lineBreakBeforeControlFlowKeywords" : false,
"lineBreakBeforeEachArgument" : true,
"lineBreakBeforeEachGenericRequirement" : true,
"lineLength" : 120,
"maximumBlankLines" : 1,
"respectsExistingLineBreaks" : true,
"prioritizeKeepingFunctionOutputTogether" : true,
"noAssignmentInExpressions" : {
"allowedFunctions" : [
"XCTAssertNoThrow",
"XCTAssertThrowsError"
]
},
"rules" : {
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLiteralForEmptyCollectionInit" : false,
"AlwaysUseLowerCamelCase" : false,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
"FullyIndirectEnum" : true,
"GroupNumericLiterals" : true,
"IdentifiersMustBeASCII" : true,
"NeverForceUnwrap" : false,
"NeverUseForceTry" : false,
"NeverUseImplicitlyUnwrappedOptionals" : false,
"NoAccessLevelOnExtensionDeclaration" : true,
"NoAssignmentInExpressions" : true,
"NoBlockComments" : true,
"NoCasesWithOnlyFallthrough" : true,
"NoEmptyTrailingClosureParentheses" : true,
"NoLabelsInCasePatterns" : true,
"NoLeadingUnderscores" : false,
"NoParensAroundConditions" : true,
"NoVoidReturnOnFunctionSignature" : true,
"OmitExplicitReturns" : true,
"OneCasePerLine" : true,
"OneVariableDeclarationPerLine" : true,
"OnlyOneTrailingClosureArgument" : true,
"OrderedImports" : true,
"ReplaceForEachWithForLoop" : true,
"ReturnVoidInsteadOfEmptyTuple" : true,
"UseEarlyExits" : false,
"UseExplicitNilCheckInConditions" : false,
"UseLetInEveryBoundCaseVariable" : false,
"UseShorthandTypeNames" : true,
"UseSingleLinePropertyGetter" : false,
"UseSynthesizedInitializer" : false,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
}
}
-24
View File
@@ -1,24 +0,0 @@
# file options
--swiftversion 5.4
--exclude .build
# format options
--self insert
--patternlet inline
--ranges nospace
--stripunusedargs unnamed-only
--ifdef no-indent
--extensionacl on-declarations
--disable typeSugar # https://github.com/nicklockwood/SwiftFormat/issues/636
--disable andOperator
--disable wrapMultilineStatementBraces
--disable enumNamespaces
--disable redundantExtensionACL
--disable redundantReturn
--disable preferKeyPath
--disable sortedSwitchCases
--disable numberFormatting
# rules
+3 -3
View File
@@ -65,10 +65,10 @@ We require that your commit messages match our template. The easiest way to do t
git config commit.template dev/git.commit.template
### Make sure Tests work on Linux
AsyncHTTPClient uses XCTest to run tests on both macOS and Linux. While the macOS version of XCTest is able to use the Objective-C runtime to discover tests at execution time, the Linux version is not.
For this reason, whenever you add new tests **you have to run a script** that generates the hooks needed to run those tests on Linux, or our CI will complain that the tests are not all present on Linux. To do this, merely execute `ruby ./scripts/generate_linux_tests.rb` at the root of the package and check the changes it made.
### Run CI checks locally
You can run the Github Actions workflows locally using [act](https://github.com/nektos/act). For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally).
## How to contribute your work
+1 -1
View File
@@ -23,7 +23,7 @@ struct GetHTML {
let request = HTTPClientRequest(url: "https://apple.com")
let response = try await httpClient.execute(request, timeout: .seconds(30))
print("HTTP head", response)
let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
print(String(buffer: body))
} catch {
print("request failed:", error)
+1 -1
View File
@@ -38,7 +38,7 @@ struct GetJSON {
let request = HTTPClientRequest(url: "https://xkcd.com/info.0.json")
let response = try await httpClient.execute(request, timeout: .seconds(30))
print("HTTP head", response)
let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
// we use an overload defined in `NIOFoundationCompat` for `decode(_:from:)` to
// efficiently decode from a `ByteBuffer`
let comic = try JSONDecoder().decode(Comic.self, from: body)
+6 -3
View File
@@ -43,7 +43,8 @@ let package = Package(
dependencies: [
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "NIOCore", package: "swift-nio"),
], path: "GetHTML"
],
path: "GetHTML"
),
.executableTarget(
name: "GetJSON",
@@ -51,14 +52,16 @@ let package = Package(
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
], path: "GetJSON"
],
path: "GetJSON"
),
.executableTarget(
name: "StreamingByteCounter",
dependencies: [
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "NIOCore", package: "swift-nio"),
], path: "StreamingByteCounter"
],
path: "StreamingByteCounter"
),
]
)
+2 -2
View File
@@ -50,13 +50,13 @@ This product contains a derivation of the Tony Stone's 'process_test_files.rb'.
* https://www.apache.org/licenses/LICENSE-2.0
* HOMEPAGE:
* https://github.com/tonystone/build-tools/commit/6c417b7569df24597a48a9aa7b505b636e8f73a1
* https://github.com/tonystone/build-tools/blob/master/source/xctest_tool.rb
* https://github.com/tonystone/build-tools/blob/cf3440f43bde2053430285b4ed0709c865892eb5/source/xctest_tool.rb
---
This product contains a derivation of Fabian Fett's 'Base64.swift'.
* LICENSE (Apache License 2.0):
* https://github.com/fabianfett/swift-base64-kit/blob/master/LICENSE
* https://github.com/swift-extras/swift-extras-base64/blob/b8af49699d59ad065b801715a5009619100245ca/LICENSE
* HOMEPAGE:
* https://github.com/fabianfett/swift-base64-kit
+2 -3
View File
@@ -18,7 +18,7 @@ import PackageDescription
let package = Package(
name: "async-http-client",
products: [
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.71.0"),
@@ -28,14 +28,13 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.19.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.4"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
],
targets: [
.target(
name: "CAsyncHTTPClient",
cSettings: [
.define("_GNU_SOURCE"),
.define("_GNU_SOURCE")
]
),
.target(
@@ -12,11 +12,12 @@
//
//===----------------------------------------------------------------------===//
import struct Foundation.URL
import Logging
import NIOCore
import NIOHTTP1
import struct Foundation.URL
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension HTTPClient {
/// Execute arbitrary HTTP requests.
@@ -85,11 +86,13 @@ extension HTTPClient {
return response
}
guard let redirectURL = response.headers.extractRedirectTarget(
status: response.status,
originalURL: preparedRequest.url,
originalScheme: preparedRequest.poolKey.scheme
) else {
guard
let redirectURL = response.headers.extractRedirectTarget(
status: response.status,
originalURL: preparedRequest.url,
originalScheme: preparedRequest.poolKey.scheme
)
else {
// response does not want a redirect
return response
}
@@ -120,31 +123,35 @@ extension HTTPClient {
) async throws -> HTTPClientResponse {
let cancelHandler = TransactionCancelHandler()
return try await withTaskCancellationHandler(operation: { () async throws -> HTTPClientResponse in
let eventLoop = self.eventLoopGroup.any()
let deadlineTask = eventLoop.scheduleTask(deadline: deadline) {
cancelHandler.cancel(reason: .deadlineExceeded)
}
defer {
deadlineTask.cancel()
}
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<HTTPClientResponse, Swift.Error>) -> Void in
let transaction = Transaction(
request: request,
requestOptions: .fromClientConfiguration(self.configuration),
logger: logger,
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
preferredEventLoop: eventLoop,
responseContinuation: continuation
)
return try await withTaskCancellationHandler(
operation: { () async throws -> HTTPClientResponse in
let eventLoop = self.eventLoopGroup.any()
let deadlineTask = eventLoop.scheduleTask(deadline: deadline) {
cancelHandler.cancel(reason: .deadlineExceeded)
}
defer {
deadlineTask.cancel()
}
return try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<HTTPClientResponse, Swift.Error>) -> Void in
let transaction = Transaction(
request: request,
requestOptions: .fromClientConfiguration(self.configuration),
logger: logger,
connectionDeadline: .now() + (self.configuration.timeout.connectionCreationTimeout),
preferredEventLoop: eventLoop,
responseContinuation: continuation
)
cancelHandler.registerTransaction(transaction)
cancelHandler.registerTransaction(transaction)
self.poolManager.executeRequest(transaction)
self.poolManager.executeRequest(transaction)
}
},
onCancel: {
cancelHandler.cancel(reason: .taskCanceled)
}
}, onCancel: {
cancelHandler.cancel(reason: .taskCanceled)
})
)
}
}
@@ -12,11 +12,12 @@
//
//===----------------------------------------------------------------------===//
import struct Foundation.URL
import NIOCore
import NIOHTTP1
import NIOSSL
import struct Foundation.URL
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension HTTPClientRequest {
struct Prepared {
@@ -81,7 +82,11 @@ extension HTTPClientRequest.Prepared.Body {
case .asyncSequence(let length, let makeAsyncIterator):
self = .asyncSequence(length: length, nextBodyPart: makeAsyncIterator())
case .sequence(let length, let canBeConsumedMultipleTimes, let makeCompleteBody):
self = .sequence(length: length, canBeConsumedMultipleTimes: canBeConsumedMultipleTimes, makeCompleteBody: makeCompleteBody)
self = .sequence(
length: length,
canBeConsumedMultipleTimes: canBeConsumedMultipleTimes,
makeCompleteBody: makeCompleteBody
)
case .byteBuffer(let byteBuffer):
self = .byteBuffer(byteBuffer)
}
@@ -176,23 +176,29 @@ extension HTTPClientRequest.Body {
// the maximum size of a ByteBuffer.
if bufferPointer.count <= byteBufferMaxSize {
let buffer = ByteBuffer(bytes: bufferPointer)
return Self(.sequence(
length: length.storage,
canBeConsumedMultipleTimes: true,
makeCompleteBody: { _ in buffer }
))
return Self(
.sequence(
length: length.storage,
canBeConsumedMultipleTimes: true,
makeCompleteBody: { _ in buffer }
)
)
} else {
// we need to copy `bufferPointer` eagerly as the pointer is only valid during the call to `withContiguousStorageIfAvailable`
let buffers: Array<ByteBuffer> = bufferPointer.chunks(ofCount: byteBufferMaxSize).map { ByteBuffer(bytes: $0) }
return Self(.asyncSequence(
length: length.storage,
makeAsyncIterator: {
var iterator = buffers.makeIterator()
return { _ in
iterator.next()
let buffers: [ByteBuffer] = bufferPointer.chunks(ofCount: byteBufferMaxSize).map {
ByteBuffer(bytes: $0)
}
return Self(
.asyncSequence(
length: length.storage,
makeAsyncIterator: {
var iterator = buffers.makeIterator()
return { _ in
iterator.next()
}
}
}
))
)
)
}
}
if let body = body {
@@ -200,21 +206,23 @@ extension HTTPClientRequest.Body {
}
// slow path
return Self(.asyncSequence(
length: length.storage
) {
var iterator = bytes.makeIterator()
return { allocator in
var buffer = allocator.buffer(capacity: bagOfBytesToByteBufferConversionChunkSize)
while buffer.writableBytes > 0, let byte = iterator.next() {
buffer.writeInteger(byte)
return Self(
.asyncSequence(
length: length.storage
) {
var iterator = bytes.makeIterator()
return { allocator in
var buffer = allocator.buffer(capacity: bagOfBytesToByteBufferConversionChunkSize)
while buffer.writableBytes > 0, let byte = iterator.next() {
buffer.writeInteger(byte)
}
if buffer.readableBytes > 0 {
return buffer
}
return nil
}
if buffer.readableBytes > 0 {
return buffer
}
return nil
}
})
)
}
/// Create an ``HTTPClientRequest/Body-swift.struct`` from a `Collection` of bytes.
@@ -237,25 +245,29 @@ extension HTTPClientRequest.Body {
length: Length
) -> Self where Bytes.Element == UInt8 {
if bytes.count <= bagOfBytesToByteBufferConversionChunkSize {
return self.init(.sequence(
length: length.storage,
canBeConsumedMultipleTimes: true
) { allocator in
allocator.buffer(bytes: bytes)
})
} else {
return self.init(.asyncSequence(
length: length.storage,
makeAsyncIterator: {
var iterator = bytes.chunks(ofCount: bagOfBytesToByteBufferConversionChunkSize).makeIterator()
return { allocator in
guard let chunk = iterator.next() else {
return nil
}
return allocator.buffer(bytes: chunk)
}
return self.init(
.sequence(
length: length.storage,
canBeConsumedMultipleTimes: true
) { allocator in
allocator.buffer(bytes: bytes)
}
))
)
} else {
return self.init(
.asyncSequence(
length: length.storage,
makeAsyncIterator: {
var iterator = bytes.chunks(ofCount: bagOfBytesToByteBufferConversionChunkSize).makeIterator()
return { allocator in
guard let chunk = iterator.next() else {
return nil
}
return allocator.buffer(bytes: chunk)
}
}
)
)
}
}
@@ -276,12 +288,14 @@ extension HTTPClientRequest.Body {
_ sequenceOfBytes: SequenceOfBytes,
length: Length
) -> Self where SequenceOfBytes.Element == ByteBuffer {
let body = self.init(.asyncSequence(length: length.storage) {
var iterator = sequenceOfBytes.makeAsyncIterator()
return { _ -> ByteBuffer? in
try await iterator.next()
let body = self.init(
.asyncSequence(length: length.storage) {
var iterator = sequenceOfBytes.makeAsyncIterator()
return { _ -> ByteBuffer? in
try await iterator.next()
}
}
})
)
return body
}
@@ -304,19 +318,21 @@ extension HTTPClientRequest.Body {
_ bytes: Bytes,
length: Length
) -> Self where Bytes.Element == UInt8 {
let body = self.init(.asyncSequence(length: length.storage) {
var iterator = bytes.makeAsyncIterator()
return { allocator -> ByteBuffer? in
var buffer = allocator.buffer(capacity: bagOfBytesToByteBufferConversionChunkSize)
while buffer.writableBytes > 0, let byte = try await iterator.next() {
buffer.writeInteger(byte)
let body = self.init(
.asyncSequence(length: length.storage) {
var iterator = bytes.makeAsyncIterator()
return { allocator -> ByteBuffer? in
var buffer = allocator.buffer(capacity: bagOfBytesToByteBufferConversionChunkSize)
while buffer.writableBytes > 0, let byte = try await iterator.next() {
buffer.writeInteger(byte)
}
if buffer.readableBytes > 0 {
return buffer
}
return nil
}
if buffer.readableBytes > 0 {
return buffer
}
return nil
}
})
)
return body
}
}
@@ -55,14 +55,16 @@ public struct HTTPClientResponse: Sendable {
version: version,
status: status,
headers: headers,
body: .init(.transaction(
body,
expectedContentLength: HTTPClientResponse.expectedContentLength(
requestMethod: requestMethod,
headers: headers,
status: status
body: .init(
.transaction(
body,
expectedContentLength: HTTPClientResponse.expectedContentLength(
requestMethod: requestMethod,
headers: headers,
status: status
)
)
))
)
)
}
}
@@ -116,7 +118,8 @@ extension HTTPClientResponse {
}
/// calling collect function within here in order to ensure the correct nested type
func collect<Body: AsyncSequence>(_ body: Body, maxBytes: Int) async throws -> ByteBuffer where Body.Element == ByteBuffer {
func collect<Body: AsyncSequence>(_ body: Body, maxBytes: Int) async throws -> ByteBuffer
where Body.Element == ByteBuffer {
try await body.collect(upTo: maxBytes)
}
return try await collect(self, maxBytes: maxBytes)
@@ -126,7 +129,11 @@ extension HTTPClientResponse {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension HTTPClientResponse {
static func expectedContentLength(requestMethod: HTTPMethod, headers: HTTPHeaders, status: HTTPResponseStatus) -> Int? {
static func expectedContentLength(
requestMethod: HTTPMethod,
headers: HTTPHeaders,
status: HTTPResponseStatus
) -> Int? {
if status == .notModified {
return 0
} else if requestMethod == .HEAD {
@@ -82,9 +82,20 @@ extension Transaction {
enum FailAction {
case none
/// fail response before head received. scheduler and executor are exclusive here.
case failResponseHead(CheckedContinuation<HTTPClientResponse, Error>, Error, HTTPRequestScheduler?, HTTPRequestExecutor?, bodyStreamContinuation: CheckedContinuation<Void, Error>?)
case failResponseHead(
CheckedContinuation<HTTPClientResponse, Error>,
Error,
HTTPRequestScheduler?,
HTTPRequestExecutor?,
bodyStreamContinuation: CheckedContinuation<Void, Error>?
)
/// fail response after response head received. fail the response stream (aka call to `next()`)
case failResponseStream(TransactionBody.Source, Error, HTTPRequestExecutor, bodyStreamContinuation: CheckedContinuation<Void, Error>?)
case failResponseStream(
TransactionBody.Source,
Error,
HTTPRequestExecutor,
bodyStreamContinuation: CheckedContinuation<Void, Error>?
)
case failRequestStreamContinuation(CheckedContinuation<Void, Error>, Error)
}
@@ -116,24 +127,41 @@ extension Transaction {
switch requestStreamState {
case .paused(continuation: .some(let continuation)):
self.state = .finished(error: error)
return .failResponseHead(context.continuation, error, nil, context.executor, bodyStreamContinuation: continuation)
return .failResponseHead(
context.continuation,
error,
nil,
context.executor,
bodyStreamContinuation: continuation
)
case .requestHeadSent, .finished, .producing, .paused(continuation: .none):
self.state = .finished(error: error)
return .failResponseHead(context.continuation, error, nil, context.executor, bodyStreamContinuation: nil)
return .failResponseHead(
context.continuation,
error,
nil,
context.executor,
bodyStreamContinuation: nil
)
}
case .executing(let context, let requestStreamState, .streamingBody(let source)):
self.state = .finished(error: error)
switch requestStreamState {
case .paused(let bodyStreamContinuation):
return .failResponseStream(source, error, context.executor, bodyStreamContinuation: bodyStreamContinuation)
return .failResponseStream(
source,
error,
context.executor,
bodyStreamContinuation: bodyStreamContinuation
)
case .finished, .producing, .requestHeadSent:
return .failResponseStream(source, error, context.executor, bodyStreamContinuation: nil)
}
case .finished(error: _),
.executing(_, _, .finished):
.executing(_, _, .finished):
return .none
}
}
@@ -165,7 +193,7 @@ extension Transaction {
return .cancel(executor)
case .executing,
.finished(error: .none):
.finished(error: .none):
preconditionFailure("Invalid state: \(self.state)")
}
}
@@ -179,7 +207,9 @@ extension Transaction {
mutating func resumeRequestBodyStream() -> ResumeProducingAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("Received a resumeBodyRequest on a request, that isn't executing. Invalid state: \(self.state)")
preconditionFailure(
"Received a resumeBodyRequest on a request, that isn't executing. Invalid state: \(self.state)"
)
case .executing(let context, .requestHeadSent, let responseState):
// the request can start to send its body.
@@ -187,7 +217,9 @@ extension Transaction {
return .startStream(context.allocator)
case .executing(_, .producing, _):
preconditionFailure("Received a resumeBodyRequest on a request, that is producing. Invalid state: \(self.state)")
preconditionFailure(
"Received a resumeBodyRequest on a request, that is producing. Invalid state: \(self.state)"
)
case .executing(let context, .paused(.none), let responseState):
// request stream is currently paused, but there is no write waiting. We don't need
@@ -213,17 +245,17 @@ extension Transaction {
mutating func pauseRequestBodyStream() {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, .requestHeadSent, _):
.queued,
.deadlineExceededWhileQueued,
.executing(_, .requestHeadSent, _):
preconditionFailure("A request stream can only be resumed, if the request was started")
case .executing(let context, .producing, let responseSteam):
self.state = .executing(context, .paused(continuation: nil), responseSteam)
case .executing(_, .paused, _),
.executing(_, .finished, _),
.finished:
.executing(_, .finished, _),
.finished:
// the channels writability changed to paused after we have already forwarded all
// request bytes. Can be ignored.
break
@@ -239,10 +271,12 @@ extension Transaction {
func writeNextRequestPart() -> NextWriteAction {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, .requestHeadSent, _):
preconditionFailure("A request stream can only produce, if the request was started. Invalid state: \(self.state)")
.queued,
.deadlineExceededWhileQueued,
.executing(_, .requestHeadSent, _):
preconditionFailure(
"A request stream can only produce, if the request was started. Invalid state: \(self.state)"
)
case .executing(let context, .producing, _):
// We are currently producing the request body. The executors channel is writable.
@@ -260,7 +294,9 @@ extension Transaction {
return .writeAndWait(context.executor)
case .executing(_, .paused(continuation: .some), _):
preconditionFailure("A write continuation already exists, but we tried to set another one. Invalid state: \(self.state)")
preconditionFailure(
"A write continuation already exists, but we tried to set another one. Invalid state: \(self.state)"
)
case .finished, .executing(_, .finished, _):
return .fail
@@ -270,11 +306,13 @@ extension Transaction {
mutating func waitForRequestBodyDemand(continuation: CheckedContinuation<Void, Error>) {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, .requestHeadSent, _),
.executing(_, .finished, _):
preconditionFailure("A request stream can only produce, if the request was started. Invalid state: \(self.state)")
.queued,
.deadlineExceededWhileQueued,
.executing(_, .requestHeadSent, _),
.executing(_, .finished, _):
preconditionFailure(
"A request stream can only produce, if the request was started. Invalid state: \(self.state)"
)
case .executing(_, .producing, _):
preconditionFailure()
@@ -303,17 +341,19 @@ extension Transaction {
mutating func finishRequestBodyStream() -> FinishAction {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, .finished, _):
.queued,
.deadlineExceededWhileQueued,
.executing(_, .finished, _):
preconditionFailure("Invalid state: \(self.state)")
case .executing(_, .paused(continuation: .some), _):
preconditionFailure("Received a request body end, while having a registered back-pressure continuation. Invalid state: \(self.state)")
preconditionFailure(
"Received a request body end, while having a registered back-pressure continuation. Invalid state: \(self.state)"
)
case .executing(let context, .producing, let responseState),
.executing(let context, .paused(continuation: .none), let responseState),
.executing(let context, .requestHeadSent, let responseState):
.executing(let context, .paused(continuation: .none), let responseState),
.executing(let context, .requestHeadSent, let responseState):
switch responseState {
case .finished:
@@ -345,10 +385,10 @@ extension Transaction {
) -> ReceiveResponseHeadAction {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, _, .streamingBody),
.executing(_, _, .finished):
.queued,
.deadlineExceededWhileQueued,
.executing(_, _, .streamingBody),
.executing(_, _, .finished):
preconditionFailure("invalid state \(self.state)")
case .executing(let context, let requestState, .waitingForResponseHead):
@@ -381,15 +421,15 @@ extension Transaction {
mutating func produceMore() -> ProduceMoreAction {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, _, .waitingForResponseHead):
.queued,
.deadlineExceededWhileQueued,
.executing(_, _, .waitingForResponseHead):
preconditionFailure("invalid state \(self.state)")
case .executing(let context, _, .streamingBody):
return .requestMoreResponseBodyParts(context.executor)
case .finished,
.executing(_, _, .finished):
.executing(_, _, .finished):
return .none
}
}
@@ -402,7 +442,9 @@ extension Transaction {
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ReceiveResponsePartAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("Received a response body part, but request hasn't started yet. Invalid state: \(self.state)")
preconditionFailure(
"Received a response body part, but request hasn't started yet. Invalid state: \(self.state)"
)
case .executing(_, _, .waitingForResponseHead):
preconditionFailure("If we receive a response body, we must have received a head before")
@@ -415,7 +457,9 @@ extension Transaction {
return .none
case .executing(_, _, .finished):
preconditionFailure("Received response end. Must not receive further body parts after that. Invalid state: \(self.state)")
preconditionFailure(
"Received response end. Must not receive further body parts after that. Invalid state: \(self.state)"
)
}
}
@@ -427,10 +471,12 @@ extension Transaction {
mutating func succeedRequest(_ newChunks: CircularBuffer<ByteBuffer>?) -> ReceiveResponseEndAction {
switch self.state {
case .initialized,
.queued,
.deadlineExceededWhileQueued,
.executing(_, _, .waitingForResponseHead):
preconditionFailure("Received no response head, but received a response end. Invalid state: \(self.state)")
.queued,
.deadlineExceededWhileQueued,
.executing(_, _, .waitingForResponseHead):
preconditionFailure(
"Received no response head, but received a response end. Invalid state: \(self.state)"
)
case .executing(let context, let requestState, .streamingBody(let source)):
self.state = .executing(context, requestState, .finished)
@@ -439,7 +485,9 @@ extension Transaction {
// the request failed or was cancelled before, we can ignore all events
return .none
case .executing(_, _, .finished):
preconditionFailure("Already received an eof or error before. Must not receive further events. Invalid state: \(self.state)")
preconditionFailure(
"Already received an eof or error before. Must not receive further events. Invalid state: \(self.state)"
)
}
}
@@ -146,8 +146,8 @@ import NIOSSL
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Transaction: HTTPSchedulableRequest {
var poolKey: ConnectionPool.Key { self.request.poolKey }
var tlsConfiguration: TLSConfiguration? { return self.request.tlsConfiguration }
var requiredEventLoop: EventLoop? { return nil }
var tlsConfiguration: TLSConfiguration? { self.request.tlsConfiguration }
var requiredEventLoop: EventLoop? { nil }
func requestWasQueued(_ scheduler: HTTPRequestScheduler) {
self.stateLock.withLock {
@@ -290,7 +290,7 @@ extension Transaction: HTTPExecutableRequest {
case .failResponseHead(let continuation, let error, let scheduler, let executor, let bodyStreamContinuation):
continuation.resume(throwing: error)
bodyStreamContinuation?.resume(throwing: error)
scheduler?.cancelRequest(self) // NOTE: scheduler and executor are exclusive here
scheduler?.cancelRequest(self) // NOTE: scheduler and executor are exclusive here
executor?.cancelRequest(self)
case .failResponseStream(let source, let error, let executor, let requestBodyStreamContinuation):
@@ -317,7 +317,7 @@ extension Transaction: HTTPExecutableRequest {
scheduler?.cancelRequest(self)
executor?.cancelRequest(self)
bodyStreamContinuation?.resume(throwing: HTTPClientError.deadlineExceeded)
case .cancelSchedulerOnly(scheduler: let scheduler):
case .cancelSchedulerOnly(let scheduler):
scheduler.cancelRequest(self)
case .none:
break
+128 -114
View File
@@ -19,142 +19,156 @@
extension String {
/// Base64 encode a collection of UInt8 to a string, without the use of Foundation.
@inlinable
init<Buffer: Collection>(base64Encoding bytes: Buffer)
where Buffer.Element == UInt8
{
self = Base64.encode(bytes: bytes)
}
/// Base64 encode a collection of UInt8 to a string, without the use of Foundation.
@inlinable
init<Buffer: Collection>(base64Encoding bytes: Buffer)
where Buffer.Element == UInt8 {
self = Base64.encode(bytes: bytes)
}
}
// swift-format-ignore: DontRepeatTypeInStaticProperties
@usableFromInline
internal struct Base64 {
@inlinable
static func encode<Buffer: Collection>(bytes: Buffer)
-> String where Buffer.Element == UInt8
{
guard !bytes.isEmpty else {
return ""
@inlinable
static func encode<Buffer: Collection>(
bytes: Buffer
)
-> String where Buffer.Element == UInt8
{
guard !bytes.isEmpty else {
return ""
}
// In Base64, 3 bytes become 4 output characters, and we pad to the
// nearest multiple of four.
let base64StringLength = ((bytes.count + 2) / 3) * 4
let alphabet = Base64.encodeBase64
return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in
var input = bytes.makeIterator()
var offset = 0
while let firstByte = input.next() {
let secondByte = input.next()
let thirdByte = input.next()
backingStorage[offset] = Base64.encode(alphabet: alphabet, firstByte: firstByte)
backingStorage[offset + 1] = Base64.encode(
alphabet: alphabet,
firstByte: firstByte,
secondByte: secondByte
)
backingStorage[offset + 2] = Base64.encode(
alphabet: alphabet,
secondByte: secondByte,
thirdByte: thirdByte
)
backingStorage[offset + 3] = Base64.encode(alphabet: alphabet, thirdByte: thirdByte)
offset += 4
}
return offset
}
}
// In Base64, 3 bytes become 4 output characters, and we pad to the
// nearest multiple of four.
let base64StringLength = ((bytes.count + 2) / 3) * 4
let alphabet = Base64.encodeBase64
return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in
var input = bytes.makeIterator()
var offset = 0
while let firstByte = input.next() {
let secondByte = input.next()
let thirdByte = input.next()
// MARK: Internal
backingStorage[offset] = Base64.encode(alphabet: alphabet, firstByte: firstByte)
backingStorage[offset + 1] = Base64.encode(alphabet: alphabet, firstByte: firstByte, secondByte: secondByte)
backingStorage[offset + 2] = Base64.encode(alphabet: alphabet, secondByte: secondByte, thirdByte: thirdByte)
backingStorage[offset + 3] = Base64.encode(alphabet: alphabet, thirdByte: thirdByte)
offset += 4
}
return offset
// The base64 unicode table.
@usableFromInline
static let encodeBase64: [UInt8] = [
UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"),
UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"),
UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"),
UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"),
UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"),
UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"),
UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"),
UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"),
UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"),
UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"),
UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"),
UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"),
UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"),
UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"),
UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"),
UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"),
]
static let encodePaddingCharacter: UInt8 = UInt8(ascii: "=")
@usableFromInline
static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 {
let index = firstByte >> 2
return alphabet[Int(index)]
}
}
// MARK: Internal
// The base64 unicode table.
@usableFromInline
static let encodeBase64: [UInt8] = [
UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"),
UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"),
UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"),
UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"),
UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"),
UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"),
UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"),
UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"),
UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"),
UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"),
UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"),
UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"),
UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"),
UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"),
UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"),
UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"),
]
static let encodePaddingCharacter: UInt8 = UInt8(ascii: "=")
@usableFromInline
static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 {
let index = firstByte >> 2
return alphabet[Int(index)]
}
@usableFromInline
static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 {
var index = (firstByte & 0b00000011) << 4
if let secondByte = secondByte {
index += (secondByte & 0b11110000) >> 4
@usableFromInline
static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 {
var index = (firstByte & 0b00000011) << 4
if let secondByte = secondByte {
index += (secondByte & 0b11110000) >> 4
}
return alphabet[Int(index)]
}
return alphabet[Int(index)]
}
@usableFromInline
static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 {
guard let secondByte = secondByte else {
// No second byte means we are just emitting padding.
return Base64.encodePaddingCharacter
@usableFromInline
static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 {
guard let secondByte = secondByte else {
// No second byte means we are just emitting padding.
return Base64.encodePaddingCharacter
}
var index = (secondByte & 0b00001111) << 2
if let thirdByte = thirdByte {
index += (thirdByte & 0b11000000) >> 6
}
return alphabet[Int(index)]
}
var index = (secondByte & 0b00001111) << 2
if let thirdByte = thirdByte {
index += (thirdByte & 0b11000000) >> 6
}
return alphabet[Int(index)]
}
@usableFromInline
static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 {
guard let thirdByte = thirdByte else {
// No third byte means just padding.
return Base64.encodePaddingCharacter
@usableFromInline
static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 {
guard let thirdByte = thirdByte else {
// No third byte means just padding.
return Base64.encodePaddingCharacter
}
let index = thirdByte & 0b00111111
return alphabet[Int(index)]
}
let index = thirdByte & 0b00111111
return alphabet[Int(index)]
}
}
extension String {
/// This is a backport of a proposed String initializer that will allow writing directly into an uninitialized String's backing memory.
///
/// As this API does not exist prior to 5.3 on Linux, or on older Apple platforms, we fake it out with a pointer and accept the extra copy.
@inlinable
init(backportUnsafeUninitializedCapacity capacity: Int,
initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer<UInt8>) throws -> Int) rethrows {
// The buffer will store zero terminated C string
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: capacity + 1)
defer {
buffer.deallocate()
/// This is a backport of a proposed String initializer that will allow writing directly into an uninitialized String's backing memory.
///
/// As this API does not exist prior to 5.3 on Linux, or on older Apple platforms, we fake it out with a pointer and accept the extra copy.
@inlinable
init(
backportUnsafeUninitializedCapacity capacity: Int,
initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer<UInt8>) throws -> Int
) rethrows {
// The buffer will store zero terminated C string
let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: capacity + 1)
defer {
buffer.deallocate()
}
let initializedCount = try initializer(buffer)
precondition(initializedCount <= capacity, "Overran buffer in initializer!")
// add zero termination
buffer[initializedCount] = 0
self = String(cString: buffer.baseAddress!)
}
let initializedCount = try initializer(buffer)
precondition(initializedCount <= capacity, "Overran buffer in initializer!")
// add zero termination
buffer[initializedCount] = 0
self = String(cString: buffer.baseAddress!)
}
}
extension String {
@inlinable
init(customUnsafeUninitializedCapacity capacity: Int,
initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer<UInt8>) throws -> Int) rethrows {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) {
try self.init(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer)
} else {
try self.init(backportUnsafeUninitializedCapacity: capacity, initializingUTF8With: initializer)
@inlinable
init(
customUnsafeUninitializedCapacity capacity: Int,
initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer<UInt8>) throws -> Int
) rethrows {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) {
try self.init(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer)
} else {
try self.init(backportUnsafeUninitializedCapacity: capacity, initializingUTF8With: initializer)
}
}
}
}
@@ -27,6 +27,6 @@ struct BestEffortHashableTLSConfiguration: Hashable {
}
static func == (lhs: BestEffortHashableTLSConfiguration, rhs: BestEffortHashableTLSConfiguration) -> Bool {
return lhs.base.bestEffortEquals(rhs.base)
lhs.base.bestEffortEquals(rhs.base)
}
}
@@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
// swift-format-ignore: DontRepeatTypeInStaticProperties
extension HTTPClient.Configuration {
/// The ``HTTPClient/Configuration`` for ``HTTPClient/shared`` which tries to mimic the platform's default or prevalent browser as closely as possible.
///
@@ -27,7 +28,7 @@ extension HTTPClient.Configuration {
/// - Linux (non-Android): Google Chrome
public static var singletonConfiguration: HTTPClient.Configuration {
// To start with, let's go with these values. Obtained from Firefox's config.
return HTTPClient.Configuration(
HTTPClient.Configuration(
certificateVerification: .fullVerification,
redirectConfiguration: .follow(max: 20, allowCycles: false),
timeout: Timeout(connect: .seconds(90), read: .seconds(90)),
+4 -4
View File
@@ -29,8 +29,7 @@ extension String {
var ipv4Address = in_addr()
var ipv6Address = in6_addr()
return self.withCString { host in
inet_pton(AF_INET, host, &ipv4Address) == 1 ||
inet_pton(AF_INET6, host, &ipv6Address) == 1
inet_pton(AF_INET, host, &ipv4Address) == 1 || inet_pton(AF_INET6, host, &ipv6Address) == 1
}
}
}
@@ -67,12 +66,13 @@ enum ConnectionPool {
switch self.connectionTarget {
case .ipAddress(let serialization, let addr):
hostDescription = "\(serialization):\(addr.port!)"
case .domain(let domain, port: let port):
case .domain(let domain, let port):
hostDescription = "\(domain):\(port)"
case .unixSocket(let socketPath):
hostDescription = socketPath
}
return "\(self.scheme)://\(hostDescription)\(self.serverNameIndicatorOverride.map { " SNI: \($0)" } ?? "") TLS-hash: \(hash) "
return
"\(self.scheme)://\(hostDescription)\(self.serverNameIndicatorOverride.map { " SNI: \($0)" } ?? "") TLS-hash: \(hash) "
}
}
}
@@ -42,7 +42,7 @@ final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHand
private var proxyEstablishedPromise: EventLoopPromise<Void>?
var proxyEstablishedFuture: EventLoopFuture<Void>? {
return self.proxyEstablishedPromise?.futureResult
self.proxyEstablishedPromise?.futureResult
}
convenience init(
@@ -53,10 +53,10 @@ final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHand
let targetHost: String
let targetPort: Int
switch target {
case .ipAddress(serialization: let serialization, address: let address):
case .ipAddress(let serialization, let address):
targetHost = serialization
targetPort = address.port!
case .domain(name: let domain, port: let port):
case .domain(name: let domain, let port):
targetHost = domain
targetPort = port
case .unixSocket:
@@ -70,10 +70,12 @@ final class HTTP1ProxyConnectHandler: ChannelDuplexHandler, RemovableChannelHand
)
}
init(targetHost: String,
targetPort: Int,
proxyAuthorization: HTTPClient.Authorization?,
deadline: NIODeadline) {
init(
targetHost: String,
targetPort: Int,
proxyAuthorization: HTTPClient.Authorization?,
deadline: NIODeadline
) {
self.targetHost = targetHost
self.targetPort = targetPort
self.proxyAuthorization = proxyAuthorization
@@ -31,7 +31,7 @@ final class SOCKSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
private var socksEstablishedPromise: EventLoopPromise<Void>?
var socksEstablishedFuture: EventLoopFuture<Void>? {
return self.socksEstablishedPromise?.futureResult
self.socksEstablishedPromise?.futureResult
}
private let deadline: NIODeadline
@@ -31,7 +31,7 @@ final class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
private var tlsEstablishedPromise: EventLoopPromise<String?>?
var tlsEstablishedFuture: EventLoopFuture<String?>? {
return self.tlsEstablishedPromise?.futureResult
self.tlsEstablishedPromise?.futureResult
}
private let deadline: NIODeadline?
@@ -100,9 +100,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
// MARK: Channel Inbound Handler
func channelActive(context: ChannelHandlerContext) {
self.logger.trace("Channel active", metadata: [
"ahc-channel-writable": "\(context.channel.isWritable)",
])
self.logger.trace(
"Channel active",
metadata: [
"ahc-channel-writable": "\(context.channel.isWritable)"
]
)
let action = self.state.channelActive(isWritable: context.channel.isWritable)
self.run(action, context: context)
@@ -116,9 +119,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
}
func channelWritabilityChanged(context: ChannelHandlerContext) {
self.logger.trace("Channel writability changed", metadata: [
"ahc-channel-writable": "\(context.channel.isWritable)",
])
self.logger.trace(
"Channel writability changed",
metadata: [
"ahc-channel-writable": "\(context.channel.isWritable)"
]
)
if let timeoutAction = self.idleWriteTimeoutStateMachine?.channelWritabilityChanged(context: context) {
self.runTimeoutAction(timeoutAction, context: context)
@@ -132,9 +138,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let httpPart = self.unwrapInboundIn(data)
self.logger.trace("HTTP response part received", metadata: [
"ahc-http-part": "\(httpPart)",
])
self.logger.trace(
"HTTP response part received",
metadata: [
"ahc-http-part": "\(httpPart)"
]
)
if let timeoutAction = self.idleReadTimeoutStateMachine?.channelRead(httpPart) {
self.runTimeoutAction(timeoutAction, context: context)
@@ -152,9 +161,12 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
self.logger.trace("Channel error caught", metadata: [
"ahc-error": "\(error)",
])
self.logger.trace(
"Channel error caught",
metadata: [
"ahc-error": "\(error)"
]
)
let action = self.state.errorHappened(error)
self.run(action, context: context)
@@ -447,7 +459,8 @@ final class HTTP1ClientChannelHandler: ChannelDuplexHandler {
// MARK: Private HTTPRequestExecutor
private func writeRequestBodyPart0(_ data: IOData, request: HTTPExecutableRequest, promise: EventLoopPromise<Void>?) {
private func writeRequestBodyPart0(_ data: IOData, request: HTTPExecutableRequest, promise: EventLoopPromise<Void>?)
{
guard self.request === request, let context = self.channelContext else {
// Because the HTTPExecutableRequest may run in a different thread to our eventLoop,
// calls from the HTTPExecutableRequest to our ChannelHandler may arrive here after
@@ -691,7 +704,9 @@ struct IdleWriteStateMachine {
self.state = .waitingForWritabilityEnabled
return .clearIdleWriteTimeoutTimer
case .waitingForWritabilityEnabled:
preconditionFailure("If the channel was writable before, then we should have been waiting for more data.")
preconditionFailure(
"If the channel was writable before, then we should have been waiting for more data."
)
case .requestEndSent:
return .none
}
@@ -39,9 +39,11 @@ final class HTTP1Connection {
let id: HTTPConnectionPool.Connection.ID
init(channel: Channel,
connectionID: HTTPConnectionPool.Connection.ID,
delegate: HTTP1ConnectionDelegate) {
init(
channel: Channel,
connectionID: HTTPConnectionPool.Connection.ID,
delegate: HTTP1ConnectionDelegate
) {
self.channel = channel
self.id = connectionID
self.delegate = delegate
@@ -80,7 +82,7 @@ final class HTTP1Connection {
}
func close(promise: EventLoopPromise<Void>?) {
return self.channel.close(mode: .all, promise: promise)
self.channel.close(mode: .all, promise: promise)
}
func close() -> EventLoopFuture<Void> {
@@ -140,7 +140,7 @@ struct HTTP1ConnectionStateMachine {
self.state = .closed
return .fireChannelError(error, closeConnection: false)
case .inRequest(var requestStateMachine, close: let close):
case .inRequest(var requestStateMachine, let close):
return self.avoidingStateMachineCoW { state -> Action in
let action = requestStateMachine.errorHappened(error)
state = .inRequest(requestStateMachine, close: close)
@@ -239,7 +239,9 @@ struct HTTP1ConnectionStateMachine {
mutating func requestCancelled(closeConnection: Bool) -> Action {
switch self.state {
case .initialized:
fatalError("This event must only happen, if the connection is leased. During startup this is impossible. Invalid state: \(self.state)")
fatalError(
"This event must only happen, if the connection is leased. During startup this is impossible. Invalid state: \(self.state)"
)
case .idle:
if closeConnection {
@@ -249,7 +251,7 @@ struct HTTP1ConnectionStateMachine {
return .wait
}
case .inRequest(var requestStateMachine, close: let close):
case .inRequest(var requestStateMachine, let close):
return self.avoidingStateMachineCoW { state -> Action in
let action = requestStateMachine.requestCancelled()
state = .inRequest(requestStateMachine, close: close || closeConnection)
@@ -415,12 +417,16 @@ extension HTTP1ConnectionStateMachine {
}
extension HTTP1ConnectionStateMachine.State {
fileprivate mutating func modify(with action: HTTPRequestStateMachine.Action) -> HTTP1ConnectionStateMachine.Action {
fileprivate mutating func modify(with action: HTTPRequestStateMachine.Action) -> HTTP1ConnectionStateMachine.Action
{
switch action {
case .sendRequestHead(let head, let sendEnd):
return .sendRequestHead(head, sendEnd: sendEnd)
case .notifyRequestHeadSendSuccessfully(let resumeRequestBodyStream, let startIdleTimer):
return .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: resumeRequestBodyStream, startIdleTimer: startIdleTimer)
return .notifyRequestHeadSendSuccessfully(
resumeRequestBodyStream: resumeRequestBodyStream,
startIdleTimer: startIdleTimer
)
case .pauseRequestBodyStream:
return .pauseRequestBodyStream
case .resumeRequestBodyStream:
@@ -458,7 +464,7 @@ extension HTTP1ConnectionStateMachine.State {
fatalError("Invalid state: \(self)")
case .idle:
fatalError("How can we fail a task, if we are idle")
case .inRequest(_, close: let close):
case .inRequest(_, let close):
if case .close(let promise) = finalAction {
self = .closing
return .failRequest(error, .close(promise))
@@ -502,7 +508,7 @@ extension HTTP1ConnectionStateMachine: CustomStringConvertible {
return ".initialized"
case .idle:
return ".idle"
case .inRequest(let request, close: let close):
case .inRequest(let request, let close):
return ".inRequest(\(request), closeAfterRequest: \(close))"
case .closing:
return ".closing"
@@ -68,8 +68,10 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
}
func handlerAdded(context: ChannelHandlerContext) {
assert(context.eventLoop === self.eventLoop,
"The handler must be added to a channel that runs on the eventLoop it was initialized with.")
assert(
context.eventLoop === self.eventLoop,
"The handler must be added to a channel that runs on the eventLoop it was initialized with."
)
self.channelContext = context
let isWritable = context.channel.isActive && context.channel.isWritable
@@ -216,7 +218,7 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
// that the request is neither failed nor finished yet
self.request!.resumeRequestBodyStream()
case .forwardResponseHead(let head, pauseRequestBodyStream: let pauseRequestBodyStream):
case .forwardResponseHead(let head, let pauseRequestBodyStream):
// We can force unwrap the request here, as we have just validated in the state machine,
// that the request is neither failed nor finished yet
self.request!.receiveResponseHead(head)
@@ -268,7 +270,10 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
self.run(self.state.headSent(), context: context)
}
private func runSuccessfulFinalAction(_ action: HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction, context: ChannelHandlerContext) {
private func runSuccessfulFinalAction(
_ action: HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction,
context: ChannelHandlerContext
) {
switch action {
case .close, .none:
// The actions returned here come from an `HTTPRequestStateMachine` that assumes http/1.1
@@ -281,7 +286,11 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
}
}
private func runFailedFinalAction(_ action: HTTPRequestStateMachine.Action.FinalFailedRequestAction, context: ChannelHandlerContext, error: Error) {
private func runFailedFinalAction(
_ action: HTTPRequestStateMachine.Action.FinalFailedRequestAction,
context: ChannelHandlerContext,
error: Error
) {
// We must close the http2 stream after the request has finished. Since the request failed,
// we have no idea what the h2 streams state was. To be on the save side, we explicitly close
// the h2 stream. This will break a reference cycle in HTTP2Connection.
@@ -368,7 +377,8 @@ final class HTTP2ClientRequestHandler: ChannelDuplexHandler {
// MARK: Private HTTPRequestExecutor
private func writeRequestBodyPart0(_ data: IOData, request: HTTPExecutableRequest, promise: EventLoopPromise<Void>?) {
private func writeRequestBodyPart0(_ data: IOData, request: HTTPExecutableRequest, promise: EventLoopPromise<Void>?)
{
guard self.request === request, let context = self.channelContext else {
// Because the HTTPExecutableRequest may run in a different thread to our eventLoop,
// calls from the HTTPExecutableRequest to our ChannelHandler may arrive here after
@@ -89,12 +89,14 @@ final class HTTP2Connection {
self.channel.closeFuture
}
init(channel: Channel,
connectionID: HTTPConnectionPool.Connection.ID,
decompression: HTTPClient.Decompression,
maximumConnectionUses: Int?,
delegate: HTTP2ConnectionDelegate,
logger: Logger) {
init(
channel: Channel,
connectionID: HTTPConnectionPool.Connection.ID,
decompression: HTTPClient.Decompression,
maximumConnectionUses: Int?,
delegate: HTTP2ConnectionDelegate,
logger: Logger
) {
self.channel = channel
self.id = connectionID
self.decompression = decompression
@@ -103,7 +105,7 @@ final class HTTP2Connection {
self.multiplexer = HTTP2StreamMultiplexer(
mode: .client,
channel: channel,
targetWindowSize: 8 * 1024 * 1024, // 8mb
targetWindowSize: 8 * 1024 * 1024, // 8mb
outboundBufferSizeHighWatermark: 8196,
outboundBufferSizeLowWatermark: 4092,
inboundStreamInitializer: { channel -> EventLoopFuture<Void> in
@@ -162,7 +164,7 @@ final class HTTP2Connection {
}
func close(promise: EventLoopPromise<Void>?) {
return self.channel.close(mode: .all, promise: promise)
self.channel.close(mode: .all, promise: promise)
}
func close() -> EventLoopFuture<Void> {
@@ -199,7 +201,11 @@ final class HTTP2Connection {
let sync = self.channel.pipeline.syncOperations
let http2Handler = NIOHTTP2Handler(mode: .client, initialSettings: Self.defaultSettings)
let idleHandler = HTTP2IdleHandler(delegate: self, logger: self.logger, maximumConnectionUses: self.maximumConnectionUses)
let idleHandler = HTTP2IdleHandler(
delegate: self,
logger: self.logger,
maximumConnectionUses: self.maximumConnectionUses
)
try sync.addHandler(http2Handler, position: .last)
try sync.addHandler(idleHandler, position: .last)
@@ -221,7 +227,8 @@ final class HTTP2Connection {
case .active:
let createStreamChannelPromise = self.channel.eventLoop.makePromise(of: Channel.self)
self.multiplexer.createStreamChannel(promise: createStreamChannelPromise) { channel -> EventLoopFuture<Void> in
self.multiplexer.createStreamChannel(promise: createStreamChannelPromise) {
channel -> EventLoopFuture<Void> in
do {
// the connection may have been asked to shutdown while we created the child. in
// this
@@ -278,7 +285,7 @@ final class HTTP2Connection {
self.state = .closing
// inform all open streams, that the currently running request should be cancelled.
self.openStreams.forEach { box in
for box in self.openStreams {
box.channel.triggerUserOutboundEvent(HTTPConnectionEvent.shutdownRequested, promise: nil)
}
@@ -184,9 +184,15 @@ extension HTTP2IdleHandler {
self.state = .active(openStreams: 0, maxStreams: maxStreams, remainingUses: remainingUses)
return .notifyConnectionNewMaxStreamsSettings(maxStreams)
case .active(openStreams: let openStreams, maxStreams: let maxStreams, remainingUses: let remainingUses):
if let newMaxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value, newMaxStreams != maxStreams {
self.state = .active(openStreams: openStreams, maxStreams: newMaxStreams, remainingUses: remainingUses)
case .active(let openStreams, let maxStreams, let remainingUses):
if let newMaxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value,
newMaxStreams != maxStreams
{
self.state = .active(
openStreams: openStreams,
maxStreams: newMaxStreams,
remainingUses: remainingUses
)
return .notifyConnectionNewMaxStreamsSettings(newMaxStreams)
}
return .nothing
@@ -20,6 +20,7 @@ import NIOPosix
import NIOSOCKS
import NIOSSL
import NIOTLS
#if canImport(Network)
import NIOTransportServices
#endif
@@ -31,14 +32,17 @@ extension HTTPConnectionPool {
let tlsConfiguration: TLSConfiguration
let sslContextCache: SSLContextCache
init(key: ConnectionPool.Key,
tlsConfiguration: TLSConfiguration?,
clientConfiguration: HTTPClient.Configuration,
sslContextCache: SSLContextCache) {
init(
key: ConnectionPool.Key,
tlsConfiguration: TLSConfiguration?,
clientConfiguration: HTTPClient.Configuration,
sslContextCache: SSLContextCache
) {
self.key = key
self.clientConfiguration = clientConfiguration
self.sslContextCache = sslContextCache
self.tlsConfiguration = tlsConfiguration ?? clientConfiguration.tlsConfiguration ?? .makeClientConfiguration()
self.tlsConfiguration =
tlsConfiguration ?? clientConfiguration.tlsConfiguration ?? .makeClientConfiguration()
}
}
}
@@ -63,7 +67,13 @@ extension HTTPConnectionPool.ConnectionFactory {
var logger = logger
logger[metadataKey: "ahc-connection-id"] = "\(connectionID)"
self.makeChannel(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop, logger: logger).whenComplete { result in
self.makeChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger
).whenComplete { result in
switch result {
case .success(.http1_1(let channel)):
do {
@@ -137,7 +147,13 @@ extension HTTPConnectionPool.ConnectionFactory {
)
}
} else {
channelFuture = self.makeNonProxiedChannel(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop, logger: logger)
channelFuture = self.makeNonProxiedChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger
)
}
// let's map `ChannelError.connectTimeout` into a `HTTPClientError.connectTimeout`
@@ -160,10 +176,22 @@ extension HTTPConnectionPool.ConnectionFactory {
) -> EventLoopFuture<NegotiatedProtocol> {
switch self.key.scheme {
case .http, .httpUnix, .unix:
return self.makePlainChannel(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop).map { .http1_1($0) }
return self.makePlainChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
).map { .http1_1($0) }
case .https, .httpsUnix:
return self.makeTLSChannel(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop, logger: logger).flatMapThrowing {
channel, negotiated in
return self.makeTLSChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger
).flatMapThrowing {
channel,
negotiated in
try self.matchALPNToHTTPVersion(negotiated, channel: channel)
}
@@ -177,7 +205,12 @@ extension HTTPConnectionPool.ConnectionFactory {
eventLoop: EventLoop
) -> EventLoopFuture<Channel> {
precondition(!self.key.scheme.usesTLS, "Unexpected scheme")
return self.makePlainBootstrap(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop).connect(target: self.key.connectionTarget)
return self.makePlainBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
).connect(target: self.key.connectionTarget)
}
private func makeHTTPProxyChannel<Requester: HTTPConnectionRequester>(
@@ -191,7 +224,12 @@ extension HTTPConnectionPool.ConnectionFactory {
// A proxy connection starts with a plain text connection to the proxy server. After
// the connection has been established with the proxy server, the connection might be
// upgraded to TLS before we send our first request.
let bootstrap = self.makePlainBootstrap(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop)
let bootstrap = self.makePlainBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
)
return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in
let encoder = HTTPRequestEncoder()
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes))
@@ -234,7 +272,12 @@ extension HTTPConnectionPool.ConnectionFactory {
// A proxy connection starts with a plain text connection to the proxy server. After
// the connection has been established with the proxy server, the connection might be
// upgraded to TLS before we send our first request.
let bootstrap = self.makePlainBootstrap(requester: requester, connectionID: connectionID, deadline: deadline, eventLoop: eventLoop)
let bootstrap = self.makePlainBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
)
return bootstrap.connect(host: proxy.host, port: proxy.port).flatMap { channel in
let socksConnectHandler = SOCKSClientHandler(targetAddress: SOCKSAddress(self.key.connectionTarget))
let socksEventHandler = SOCKSEventsHandler(deadline: deadline)
@@ -319,15 +362,26 @@ extension HTTPConnectionPool.ConnectionFactory {
eventLoop: EventLoop
) -> NIOClientTCPBootstrapProtocol {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) {
return tsBootstrap
.channelOption(NIOTSChannelOptions.waitForActivity, value: self.clientConfiguration.networkFrameworkWaitForConnectivity)
.channelOption(NIOTSChannelOptions.multipathServiceType, value: self.clientConfiguration.enableMultipath ? .handover : .disabled)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *),
let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop)
{
return
tsBootstrap
.channelOption(
NIOTSChannelOptions.waitForActivity,
value: self.clientConfiguration.networkFrameworkWaitForConnectivity
)
.channelOption(
NIOTSChannelOptions.multipathServiceType,
value: self.clientConfiguration.enableMultipath ? .handover : .disabled
)
.connectTimeout(deadline - NIODeadline.now())
.channelInitializer { channel in
do {
try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler())
try channel.pipeline.syncOperations.addHandler(NWWaitingHandler(requester: requester, connectionID: connectionID))
try channel.pipeline.syncOperations.addHandler(
NWWaitingHandler(requester: requester, connectionID: connectionID)
)
return channel.eventLoop.makeSucceededVoidFuture()
} catch {
return channel.eventLoop.makeFailedFuture(error)
@@ -337,7 +391,8 @@ extension HTTPConnectionPool.ConnectionFactory {
#endif
if let nioBootstrap = ClientBootstrap(validatingGroup: eventLoop) {
return nioBootstrap
return
nioBootstrap
.connectTimeout(deadline - NIODeadline.now())
.enableMPTCP(clientConfiguration.enableMultipath)
}
@@ -362,7 +417,7 @@ extension HTTPConnectionPool.ConnectionFactory {
)
var channelFuture = bootstrapFuture.flatMap { bootstrap -> EventLoopFuture<Channel> in
return bootstrap.connect(target: self.key.connectionTarget)
bootstrap.connect(target: self.key.connectionTarget)
}.flatMap { channel -> EventLoopFuture<(Channel, String?)> in
do {
// if the channel is closed before flatMap is executed, all ChannelHandler are removed
@@ -375,7 +430,10 @@ extension HTTPConnectionPool.ConnectionFactory {
channel.pipeline.removeHandler(tlsEventHandler).map { (channel, negotiated) }
}
} catch {
assert(channel.isActive == false, "if the channel is still active then TLSEventsHandler must be present but got error \(error)")
assert(
channel.isActive == false,
"if the channel is still active then TLSEventsHandler must be present but got error \(error)"
)
return channel.eventLoop.makeFailedFuture(HTTPClientError.remoteConnectionClosed)
}
}
@@ -410,20 +468,33 @@ extension HTTPConnectionPool.ConnectionFactory {
}
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) {
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *),
let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop)
{
// create NIOClientTCPBootstrap with NIOTS TLS provider
let bootstrapFuture = tlsConfig.getNWProtocolTLSOptions(on: eventLoop, serverNameIndicatorOverride: key.serverNameIndicatorOverride).map {
let bootstrapFuture = tlsConfig.getNWProtocolTLSOptions(
on: eventLoop,
serverNameIndicatorOverride: key.serverNameIndicatorOverride
).map {
options -> NIOClientTCPBootstrapProtocol in
tsBootstrap
.channelOption(NIOTSChannelOptions.waitForActivity, value: self.clientConfiguration.networkFrameworkWaitForConnectivity)
.channelOption(NIOTSChannelOptions.multipathServiceType, value: self.clientConfiguration.enableMultipath ? .handover : .disabled)
.channelOption(
NIOTSChannelOptions.waitForActivity,
value: self.clientConfiguration.networkFrameworkWaitForConnectivity
)
.channelOption(
NIOTSChannelOptions.multipathServiceType,
value: self.clientConfiguration.enableMultipath ? .handover : .disabled
)
.connectTimeout(deadline - NIODeadline.now())
.tlsOptions(options)
.channelInitializer { channel in
do {
try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler())
try channel.pipeline.syncOperations.addHandler(NWWaitingHandler(requester: requester, connectionID: connectionID))
try channel.pipeline.syncOperations.addHandler(
NWWaitingHandler(requester: requester, connectionID: connectionID)
)
// we don't need to set a TLS deadline for NIOTS connections, since the
// TLS handshake is part of the TS connection bootstrap. If the TLS
// handshake times out the complete connection creation will be failed.
@@ -39,9 +39,11 @@ extension HTTPConnectionPool {
private let sslContextCache = SSLContextCache()
init(eventLoopGroup: EventLoopGroup,
configuration: HTTPClient.Configuration,
backgroundActivityLogger logger: Logger) {
init(
eventLoopGroup: EventLoopGroup,
configuration: HTTPClient.Configuration,
backgroundActivityLogger logger: Logger
) {
self.eventLoopGroup = eventLoopGroup
self.configuration = configuration
self.logger = logger
@@ -118,7 +120,7 @@ extension HTTPConnectionPool {
promise?.succeed(false)
case .shutdown(let pools):
pools.values.forEach { pool in
for pool in pools.values {
pool.shutdown()
}
}
@@ -140,7 +142,9 @@ extension HTTPConnectionPool.Manager: HTTPConnectionPoolDelegate {
case .shuttingDown(let promise, let soFarUnclean):
guard self._pools.removeValue(forKey: pool.key) === pool else {
preconditionFailure("Expected that the pool was created by this manager and is known for this reason.")
preconditionFailure(
"Expected that the pool was created by this manager and is known for this reason."
)
}
if self._pools.isEmpty {
@@ -154,7 +158,7 @@ extension HTTPConnectionPool.Manager: HTTPConnectionPoolDelegate {
}
switch closeAction {
case .close(let promise, unclean: let unclean):
case .close(let promise, let unclean):
promise?.succeed(unclean)
case .wait:
break
@@ -173,7 +177,7 @@ extension HTTPConnectionPool.Connection.ID {
}
func next() -> Int {
return self.atomic.loadThenWrappingIncrement(ordering: .relaxed)
self.atomic.loadThenWrappingIncrement(ordering: .relaxed)
}
}
}
@@ -44,14 +44,16 @@ final class HTTPConnectionPool {
let delegate: HTTPConnectionPoolDelegate
init(eventLoopGroup: EventLoopGroup,
sslContextCache: SSLContextCache,
tlsConfiguration: TLSConfiguration?,
clientConfiguration: HTTPClient.Configuration,
key: ConnectionPool.Key,
delegate: HTTPConnectionPoolDelegate,
idGenerator: Connection.ID.Generator,
backgroundActivityLogger logger: Logger) {
init(
eventLoopGroup: EventLoopGroup,
sslContextCache: SSLContextCache,
tlsConfiguration: TLSConfiguration?,
clientConfiguration: HTTPClient.Configuration,
key: ConnectionPool.Key,
delegate: HTTPConnectionPoolDelegate,
idGenerator: Connection.ID.Generator,
backgroundActivityLogger logger: Logger
) {
self.eventLoopGroup = eventLoopGroup
self.connectionFactory = ConnectionFactory(
key: key,
@@ -70,7 +72,8 @@ final class HTTPConnectionPool {
self._state = StateMachine(
idGenerator: idGenerator,
maximumConcurrentHTTP1Connections: clientConfiguration.connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit,
maximumConcurrentHTTP1Connections: clientConfiguration.connectionPool
.concurrentHTTP1ConnectionsPerHostSoftLimit,
retryConnectionEstablishment: clientConfiguration.connectionPool.retryConnectionEstablishment,
preferHTTP1: clientConfiguration.httpVersion == .http1Only,
maximumConnectionUses: clientConfiguration.maximumUsesPerConnection
@@ -150,7 +153,7 @@ final class HTTPConnectionPool {
self.unlocked = Unlocked(connection: .none, request: .none)
switch stateMachineAction.request {
case .executeRequest(let request, let connection, cancelTimeout: let cancelTimeout):
case .executeRequest(let request, let connection, let cancelTimeout):
if cancelTimeout {
self.locked.request = .cancelRequestTimeout(request.id)
}
@@ -158,7 +161,7 @@ final class HTTPConnectionPool {
case .executeRequestsAndCancelTimeouts(let requests, let connection):
self.locked.request = .cancelRequestTimeouts(requests)
self.unlocked.request = .executeRequests(requests, connection)
case .failRequest(let request, let error, cancelTimeout: let cancelTimeout):
case .failRequest(let request, let error, let cancelTimeout):
if cancelTimeout {
self.locked.request = .cancelRequestTimeout(request.id)
}
@@ -175,15 +178,15 @@ final class HTTPConnectionPool {
switch stateMachineAction.connection {
case .createConnection(let connectionID, on: let eventLoop):
self.unlocked.connection = .createConnection(connectionID, on: eventLoop)
case .scheduleBackoffTimer(let connectionID, backoff: let backoff, on: let eventLoop):
case .scheduleBackoffTimer(let connectionID, let backoff, on: let eventLoop):
self.locked.connection = .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop)
case .scheduleTimeoutTimer(let connectionID, on: let eventLoop):
self.locked.connection = .scheduleTimeoutTimer(connectionID, on: eventLoop)
case .cancelTimeoutTimer(let connectionID):
self.locked.connection = .cancelTimeoutTimer(connectionID)
case .closeConnection(let connection, isShutdown: let isShutdown):
case .closeConnection(let connection, let isShutdown):
self.unlocked.connection = .closeConnection(connection, isShutdown: isShutdown)
case .cleanupConnections(var cleanupContext, isShutdown: let isShutdown):
case .cleanupConnections(var cleanupContext, let isShutdown):
//
self.locked.connection = .cancelBackoffTimers(cleanupContext.connectBackoff)
cleanupContext.connectBackoff = []
@@ -221,7 +224,7 @@ final class HTTPConnectionPool {
private func runLockedConnectionAction(_ action: Actions.ConnectionAction.Locked) {
switch action {
case .scheduleBackoffTimer(let connectionID, backoff: let backoff, on: let eventLoop):
case .scheduleBackoffTimer(let connectionID, let backoff, on: let eventLoop):
self.scheduleConnectionStartBackoffTimer(connectionID, backoff, on: eventLoop)
case .scheduleTimeoutTimer(let connectionID, on: let eventLoop):
@@ -249,7 +252,7 @@ final class HTTPConnectionPool {
self.cancelRequestTimeout(requestID)
case .cancelRequestTimeouts(let requests):
requests.forEach { self.cancelRequestTimeout($0.id) }
for request in requests { self.cancelRequestTimeout(request.id) }
case .none:
break
@@ -266,10 +269,13 @@ final class HTTPConnectionPool {
case .createConnection(let connectionID, let eventLoop):
self.createConnection(connectionID, on: eventLoop)
case .closeConnection(let connection, isShutdown: let isShutdown):
self.logger.trace("close connection", metadata: [
"ahc-connection-id": "\(connection.id)",
])
case .closeConnection(let connection, let isShutdown):
self.logger.trace(
"close connection",
metadata: [
"ahc-connection-id": "\(connection.id)"
]
)
// we are not interested in the close promise...
connection.close(promise: nil)
@@ -278,7 +284,7 @@ final class HTTPConnectionPool {
self.delegate.connectionPoolDidShutdown(self, unclean: unclean)
}
case .cleanupConnections(let cleanupContext, isShutdown: let isShutdown):
case .cleanupConnections(let cleanupContext, let isShutdown):
for connection in cleanupContext.close {
connection.close(promise: nil)
}
@@ -315,13 +321,13 @@ final class HTTPConnectionPool {
connection.executeRequest(request.req)
case .executeRequests(let requests, let connection):
requests.forEach { connection.executeRequest($0.req) }
for request in requests { connection.executeRequest(request.req) }
case .failRequest(let request, let error):
request.req.fail(error)
case .failRequests(let requests, let error):
requests.forEach { $0.req.fail(error) }
for request in requests { request.req.fail(error) }
case .none:
break
@@ -329,9 +335,12 @@ final class HTTPConnectionPool {
}
private func createConnection(_ connectionID: Connection.ID, on eventLoop: EventLoop) {
self.logger.trace("Opening fresh connection", metadata: [
"ahc-connection-id": "\(connectionID)",
])
self.logger.trace(
"Opening fresh connection",
metadata: [
"ahc-connection-id": "\(connectionID)"
]
)
// Even though this function is called make it actually creates/establishes a connection.
// TBD: Should we rename it? To what?
self.connectionFactory.makeConnection(
@@ -374,9 +383,12 @@ final class HTTPConnectionPool {
}
private func scheduleIdleTimerForConnection(_ connectionID: Connection.ID, on eventLoop: EventLoop) {
self.logger.trace("Schedule idle connection timeout timer", metadata: [
"ahc-connection-id": "\(connectionID)",
])
self.logger.trace(
"Schedule idle connection timeout timer",
metadata: [
"ahc-connection-id": "\(connectionID)"
]
)
let scheduled = eventLoop.scheduleTask(in: self.idleConnectionTimeout) {
// there might be a race between a cancelTimer call and the triggering
// of this scheduled task. both want to acquire the lock
@@ -394,9 +406,12 @@ final class HTTPConnectionPool {
}
private func cancelIdleTimerForConnection(_ connectionID: Connection.ID) {
self.logger.trace("Cancel idle connection timeout timer", metadata: [
"ahc-connection-id": "\(connectionID)",
])
self.logger.trace(
"Cancel idle connection timeout timer",
metadata: [
"ahc-connection-id": "\(connectionID)"
]
)
guard let cancelTimer = self._idleTimer.removeValue(forKey: connectionID) else {
preconditionFailure("Expected to have an idle timer for connection \(connectionID) at this point.")
}
@@ -408,9 +423,12 @@ final class HTTPConnectionPool {
_ timeAmount: TimeAmount,
on eventLoop: EventLoop
) {
self.logger.trace("Schedule connection creation backoff timer", metadata: [
"ahc-connection-id": "\(connectionID)",
])
self.logger.trace(
"Schedule connection creation backoff timer",
metadata: [
"ahc-connection-id": "\(connectionID)"
]
)
let scheduled = eventLoop.scheduleTask(in: timeAmount) {
// there might be a race between a backoffTimer and the pool shutting down.
@@ -439,41 +457,53 @@ final class HTTPConnectionPool {
extension HTTPConnectionPool: HTTPConnectionRequester {
func http1ConnectionCreated(_ connection: HTTP1Connection) {
self.logger.trace("successfully created connection", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/1.1",
])
self.logger.trace(
"successfully created connection",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/1.1",
]
)
self.modifyStateAndRunActions {
$0.newHTTP1ConnectionCreated(.http1_1(connection))
}
}
func http2ConnectionCreated(_ connection: HTTP2Connection, maximumStreams: Int) {
self.logger.trace("successfully created connection", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
"ahc-max-streams": "\(maximumStreams)",
])
self.logger.trace(
"successfully created connection",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
"ahc-max-streams": "\(maximumStreams)",
]
)
self.modifyStateAndRunActions {
$0.newHTTP2ConnectionCreated(.http2(connection), maxConcurrentStreams: maximumStreams)
}
}
func failedToCreateHTTPConnection(_ connectionID: HTTPConnectionPool.Connection.ID, error: Error) {
self.logger.debug("connection attempt failed", metadata: [
"ahc-error": "\(error)",
"ahc-connection-id": "\(connectionID)",
])
self.logger.debug(
"connection attempt failed",
metadata: [
"ahc-error": "\(error)",
"ahc-connection-id": "\(connectionID)",
]
)
self.modifyStateAndRunActions {
$0.failedToCreateNewConnection(error, connectionID: connectionID)
}
}
func waitingForConnectivity(_ connectionID: HTTPConnectionPool.Connection.ID, error: Error) {
self.logger.debug("waiting for connectivity", metadata: [
"ahc-error": "\(error)",
"ahc-connection-id": "\(connectionID)",
])
self.logger.debug(
"waiting for connectivity",
metadata: [
"ahc-error": "\(error)",
"ahc-connection-id": "\(connectionID)",
]
)
self.modifyStateAndRunActions {
$0.waitingForConnectivity(error, connectionID: connectionID)
}
@@ -482,20 +512,26 @@ extension HTTPConnectionPool: HTTPConnectionRequester {
extension HTTPConnectionPool: HTTP1ConnectionDelegate {
func http1ConnectionClosed(_ connection: HTTP1Connection) {
self.logger.debug("connection closed", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/1.1",
])
self.logger.debug(
"connection closed",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/1.1",
]
)
self.modifyStateAndRunActions {
$0.http1ConnectionClosed(connection.id)
}
}
func http1ConnectionReleased(_ connection: HTTP1Connection) {
self.logger.trace("releasing connection", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/1.1",
])
self.logger.trace(
"releasing connection",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/1.1",
]
)
self.modifyStateAndRunActions {
$0.http1ConnectionReleased(connection.id)
}
@@ -504,41 +540,53 @@ extension HTTPConnectionPool: HTTP1ConnectionDelegate {
extension HTTPConnectionPool: HTTP2ConnectionDelegate {
func http2Connection(_ connection: HTTP2Connection, newMaxStreamSetting: Int) {
self.logger.debug("new max stream setting", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
"ahc-max-streams": "\(newMaxStreamSetting)",
])
self.logger.debug(
"new max stream setting",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
"ahc-max-streams": "\(newMaxStreamSetting)",
]
)
self.modifyStateAndRunActions {
$0.newHTTP2MaxConcurrentStreamsReceived(connection.id, newMaxStreams: newMaxStreamSetting)
}
}
func http2ConnectionGoAwayReceived(_ connection: HTTP2Connection) {
self.logger.debug("connection go away received", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
])
self.logger.debug(
"connection go away received",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
]
)
self.modifyStateAndRunActions {
$0.http2ConnectionGoAwayReceived(connection.id)
}
}
func http2ConnectionClosed(_ connection: HTTP2Connection) {
self.logger.debug("connection closed", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
])
self.logger.debug(
"connection closed",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
]
)
self.modifyStateAndRunActions {
$0.http2ConnectionClosed(connection.id)
}
}
func http2ConnectionStreamClosed(_ connection: HTTP2Connection, availableStreams: Int) {
self.logger.trace("stream closed", metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
])
self.logger.trace(
"stream closed",
metadata: [
"ahc-connection-id": "\(connection.id)",
"ahc-http-version": "http/2",
]
)
self.modifyStateAndRunActions {
$0.http2ConnectionStreamClosed(connection.id)
}
@@ -642,7 +690,9 @@ extension HTTPConnectionPool {
return lhsConn.id == rhsConn.id
case (.http2(let lhsConn), .http2(let rhsConn)):
return lhsConn.id == rhsConn.id
case (.__testOnly_connection(let lhsID, let lhsEventLoop), .__testOnly_connection(let rhsID, let rhsEventLoop)):
case (
.__testOnly_connection(let lhsID, let lhsEventLoop), .__testOnly_connection(let rhsID, let rhsEventLoop)
):
return lhsID == rhsID && lhsEventLoop === rhsEventLoop
default:
return false
@@ -723,7 +773,7 @@ struct EventLoopID: Hashable {
}
static func __testOnly_fakeID(_ id: Int) -> EventLoopID {
return EventLoopID(.__testOnly_fakeID(id))
EventLoopID(.__testOnly_fakeID(id))
}
}
@@ -104,8 +104,8 @@ extension HTTPRequestStateMachine {
// forwarded to the user.
case .waitingForRead,
.waitingForDemand,
.waitingForReadOrDemand:
.waitingForDemand,
.waitingForReadOrDemand:
return nil
case .modifying:
@@ -174,8 +174,8 @@ extension HTTPRequestStateMachine {
return (buffer, .none)
case .waitingForReadOrDemand(let buffer),
.waitingForRead(let buffer),
.waitingForDemand(let buffer):
.waitingForRead(let buffer),
.waitingForDemand(let buffer):
// Normally this code path should never be hit. However there is one way to trigger
// this:
//
@@ -161,10 +161,10 @@ struct HTTPRequestStateMachine {
switch self.state {
case .initialized,
.running(.streaming(_, _, producer: .producing), _),
.running(.endSent, _),
.finished,
.failed:
.running(.streaming(_, _, producer: .producing), _),
.running(.endSent, _),
.finished,
.failed:
return .wait
case .waitForChannelToBecomeWritable(let head, let metadata):
@@ -196,11 +196,11 @@ struct HTTPRequestStateMachine {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable,
.running(.streaming(_, _, producer: .paused), _),
.running(.endSent, _),
.finished,
.failed:
.waitForChannelToBecomeWritable,
.running(.streaming(_, _, producer: .paused), _),
.running(.endSent, _),
.finished,
.failed:
return .wait
case .running(.streaming(let expectedBodyLength, let sentBodyBytes, producer: .producing), let responseState):
@@ -219,13 +219,16 @@ struct HTTPRequestStateMachine {
mutating func errorHappened(_ error: Error) -> Action {
if let error = error as? NIOSSLError,
error == .uncleanShutdown,
let action = self.handleNIOSSLUncleanShutdownError() {
error == .uncleanShutdown,
let action = self.handleNIOSSLUncleanShutdownError()
{
return action
}
switch self.state {
case .initialized:
preconditionFailure("After the state machine has been initialized, start must be called immediately. Thus this state is unreachable")
preconditionFailure(
"After the state machine has been initialized, start must be called immediately. Thus this state is unreachable"
)
case .waitForChannelToBecomeWritable:
// the request failed, before it was sent onto the wire.
self.state = .failed(error)
@@ -247,14 +250,14 @@ struct HTTPRequestStateMachine {
private mutating func handleNIOSSLUncleanShutdownError() -> Action? {
switch self.state {
case .running(.streaming, .waitingForHead),
.running(.endSent, .waitingForHead):
.running(.endSent, .waitingForHead):
// if we received a NIOSSL.uncleanShutdown before we got an answer we should handle
// this like a normal connection close. We will receive a call to channelInactive after
// this error.
return .wait
case .running(.streaming, .receivingBody(let responseHead, _)),
.running(.endSent, .receivingBody(let responseHead, _)):
.running(.endSent, .receivingBody(let responseHead, _)):
// This code is only reachable for request and responses, which we expect to have a body.
// We depend on logic from the HTTPResponseDecoder here. The decoder will emit an
// HTTPResponsePart.end right after the HTTPResponsePart.head, for every request with a
@@ -263,7 +266,9 @@ struct HTTPRequestStateMachine {
// For this reason we only need to check the "content-length" or "transfer-encoding"
// headers here to determine if we are potentially in an EOF terminated response.
if responseHead.headers.contains(name: "content-length") || responseHead.headers.contains(name: "transfer-encoding") {
if responseHead.headers.contains(name: "content-length")
|| responseHead.headers.contains(name: "transfer-encoding")
{
// If we have already received the response head, the parser will ensure that we
// receive a complete response, if the content-length or transfer-encoding header
// was set. In this case we can ignore the NIOSSLError.uncleanShutdown. We will see
@@ -285,9 +290,11 @@ struct HTTPRequestStateMachine {
mutating func requestStreamPartReceived(_ part: IOData, promise: EventLoopPromise<Void>?) -> Action {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable,
.running(.endSent, _):
preconditionFailure("We must be in the request streaming phase, if we receive further body parts. Invalid state: \(self.state)")
.waitForChannelToBecomeWritable,
.running(.endSent, _):
preconditionFailure(
"We must be in the request streaming phase, if we receive further body parts. Invalid state: \(self.state)"
)
case .running(.streaming(_, _, let producerState), .receivingBody(let head, _)) where head.status.code >= 300:
// If we have already received a response head with status >= 300, we won't send out any
@@ -349,9 +356,11 @@ struct HTTPRequestStateMachine {
mutating func requestStreamFinished(promise: EventLoopPromise<Void>?) -> Action {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable,
.running(.endSent, _):
preconditionFailure("A request body stream end is only expected if we are in state request streaming. Invalid state: \(self.state)")
.waitForChannelToBecomeWritable,
.running(.endSent, _):
preconditionFailure(
"A request body stream end is only expected if we are in state request streaming. Invalid state: \(self.state)"
)
case .running(.streaming(let expectedBodyLength, let sentBodyBytes, _), .waitingForHead):
if let expected = expectedBodyLength, expected != sentBodyBytes {
@@ -363,7 +372,10 @@ struct HTTPRequestStateMachine {
self.state = .running(.endSent, .waitingForHead)
return .sendRequestEnd(promise)
case .running(.streaming(let expectedBodyLength, let sentBodyBytes, _), .receivingBody(let head, let streamState)):
case .running(
.streaming(let expectedBodyLength, let sentBodyBytes, _),
.receivingBody(let head, let streamState)
):
assert(head.status.code < 300)
if let expected = expectedBodyLength, expected != sentBodyBytes {
@@ -456,11 +468,11 @@ struct HTTPRequestStateMachine {
mutating func read() -> Action {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable,
.running(_, .waitingForHead),
.running(_, .endReceived),
.finished,
.failed:
.waitForChannelToBecomeWritable,
.running(_, .waitingForHead),
.running(_, .endReceived),
.finished,
.failed:
// If we are not in the middle of streaming the response body, we always want to get
// more data...
return .read
@@ -493,11 +505,11 @@ struct HTTPRequestStateMachine {
mutating func channelReadComplete() -> Action {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable,
.running(_, .waitingForHead),
.running(_, .endReceived),
.finished,
.failed:
.waitForChannelToBecomeWritable,
.running(_, .waitingForHead),
.running(_, .endReceived),
.finished,
.failed:
return .wait
case .running(let requestState, .receivingBody(let head, var streamState)):
@@ -528,7 +540,9 @@ struct HTTPRequestStateMachine {
switch self.state {
case .initialized, .waitForChannelToBecomeWritable:
preconditionFailure("How can we receive a response head before sending a request head ourselves \(self.state)")
preconditionFailure(
"How can we receive a response head before sending a request head ourselves \(self.state)"
)
case .running(.streaming(let expectedBodyLength, let sentBodyBytes, producer: .paused), .waitingForHead):
self.state = .running(
@@ -546,7 +560,11 @@ struct HTTPRequestStateMachine {
return .forwardResponseHead(head, pauseRequestBodyStream: true)
} else {
self.state = .running(
.streaming(expectedBodyLength: expectedBodyLength, sentBodyBytes: sentBodyBytes, producer: .producing),
.streaming(
expectedBodyLength: expectedBodyLength,
sentBodyBytes: sentBodyBytes,
producer: .producing
),
.receivingBody(head, .init())
)
return .forwardResponseHead(head, pauseRequestBodyStream: false)
@@ -557,7 +575,9 @@ struct HTTPRequestStateMachine {
return .forwardResponseHead(head, pauseRequestBodyStream: false)
case .running(_, .receivingBody), .running(_, .endReceived), .finished:
preconditionFailure("How can we successfully finish the request, before having received a head. Invalid state: \(self.state)")
preconditionFailure(
"How can we successfully finish the request, before having received a head. Invalid state: \(self.state)"
)
case .failed:
return .wait
@@ -569,10 +589,14 @@ struct HTTPRequestStateMachine {
mutating func receivedHTTPResponseBodyPart(_ body: ByteBuffer) -> Action {
switch self.state {
case .initialized, .waitForChannelToBecomeWritable:
preconditionFailure("How can we receive a response head before completely sending a request head ourselves. Invalid state: \(self.state)")
preconditionFailure(
"How can we receive a response head before completely sending a request head ourselves. Invalid state: \(self.state)"
)
case .running(_, .waitingForHead):
preconditionFailure("How can we receive a response body, if we haven't received a head. Invalid state: \(self.state)")
preconditionFailure(
"How can we receive a response body, if we haven't received a head. Invalid state: \(self.state)"
)
case .running(let requestState, .receivingBody(let head, var responseStreamState)):
return self.avoidingStateMachineCoW { state -> Action in
@@ -582,7 +606,9 @@ struct HTTPRequestStateMachine {
}
case .running(_, .endReceived), .finished:
preconditionFailure("How can we successfully finish the request, before having received a head. Invalid state: \(self.state)")
preconditionFailure(
"How can we successfully finish the request, before having received a head. Invalid state: \(self.state)"
)
case .failed:
return .wait
@@ -595,20 +621,31 @@ struct HTTPRequestStateMachine {
private mutating func receivedHTTPResponseEnd() -> Action {
switch self.state {
case .initialized, .waitForChannelToBecomeWritable:
preconditionFailure("How can we receive a response end before completely sending a request head ourselves. Invalid state: \(self.state)")
preconditionFailure(
"How can we receive a response end before completely sending a request head ourselves. Invalid state: \(self.state)"
)
case .running(_, .waitingForHead):
preconditionFailure("How can we receive a response end, if we haven't a received a head. Invalid state: \(self.state)")
preconditionFailure(
"How can we receive a response end, if we haven't a received a head. Invalid state: \(self.state)"
)
case .running(.streaming(let expectedBodyLength, let sentBodyBytes, let producerState), .receivingBody(let head, var responseStreamState))
where head.status.code < 300:
case .running(
.streaming(let expectedBodyLength, let sentBodyBytes, let producerState),
.receivingBody(let head, var responseStreamState)
)
where head.status.code < 300:
return self.avoidingStateMachineCoW { state -> Action in
let (remainingBuffer, connectionAction) = responseStreamState.end()
switch connectionAction {
case .none:
state = .running(
.streaming(expectedBodyLength: expectedBodyLength, sentBodyBytes: sentBodyBytes, producer: producerState),
.streaming(
expectedBodyLength: expectedBodyLength,
sentBodyBytes: sentBodyBytes,
producer: producerState
),
.endReceived
)
return .forwardResponseBodyParts(remainingBuffer)
@@ -624,7 +661,10 @@ struct HTTPRequestStateMachine {
case .running(.streaming(_, _, let producerState), .receivingBody(let head, var responseStreamState)):
assert(head.status.code >= 300)
assert(producerState == .paused, "Expected to have paused the request body stream, when the head was received. Invalid state: \(self.state)")
assert(
producerState == .paused,
"Expected to have paused the request body stream, when the head was received. Invalid state: \(self.state)"
)
return self.avoidingStateMachineCoW { state -> Action in
// We can ignore the connectionAction from the responseStreamState, since the
@@ -647,7 +687,9 @@ struct HTTPRequestStateMachine {
}
case .running(_, .endReceived), .finished:
preconditionFailure("How can we receive a response end, if another one was already received. Invalid state: \(self.state)")
preconditionFailure(
"How can we receive a response end, if another one was already received. Invalid state: \(self.state)"
)
case .failed:
return .wait
@@ -660,9 +702,11 @@ struct HTTPRequestStateMachine {
mutating func demandMoreResponseBodyParts() -> Action {
switch self.state {
case .initialized,
.running(_, .waitingForHead),
.waitForChannelToBecomeWritable:
preconditionFailure("The response is expected to only ask for more data after the response head was forwarded \(self.state)")
.running(_, .waitingForHead),
.waitForChannelToBecomeWritable:
preconditionFailure(
"The response is expected to only ask for more data after the response head was forwarded \(self.state)"
)
case .running(let requestState, .receivingBody(let head, var responseStreamState)):
return self.avoidingStateMachineCoW { state -> Action in
@@ -672,8 +716,8 @@ struct HTTPRequestStateMachine {
}
case .running(_, .endReceived),
.finished,
.failed:
.finished,
.failed:
return .wait
case .modifying:
@@ -684,9 +728,11 @@ struct HTTPRequestStateMachine {
mutating func idleReadTimeoutTriggered() -> Action {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable,
.running(.streaming, _):
preconditionFailure("We only schedule idle read timeouts after we have sent the complete request. Invalid state: \(self.state)")
.waitForChannelToBecomeWritable,
.running(.streaming, _):
preconditionFailure(
"We only schedule idle read timeouts after we have sent the complete request. Invalid state: \(self.state)"
)
case .running(.endSent, .waitingForHead), .running(.endSent, .receivingBody):
let error = HTTPClientError.readTimeout
@@ -707,8 +753,10 @@ struct HTTPRequestStateMachine {
mutating func idleWriteTimeoutTriggered() -> Action {
switch self.state {
case .initialized,
.waitForChannelToBecomeWritable:
preconditionFailure("We only schedule idle write timeouts while the request is being sent. Invalid state: \(self.state)")
.waitForChannelToBecomeWritable:
preconditionFailure(
"We only schedule idle write timeouts while the request is being sent. Invalid state: \(self.state)"
)
case .running(.streaming, _):
let error = HTTPClientError.writeTimeout
@@ -733,7 +781,10 @@ struct HTTPRequestStateMachine {
self.state = .running(.endSent, .waitingForHead)
return .sendRequestHead(head, sendEnd: true)
} else {
self.state = .running(.streaming(expectedBodyLength: length, sentBodyBytes: 0, producer: .paused), .waitingForHead)
self.state = .running(
.streaming(expectedBodyLength: length, sentBodyBytes: 0, producer: .paused),
.waitingForHead
)
return .sendRequestHead(head, sendEnd: false)
}
}
@@ -745,11 +796,14 @@ struct HTTPRequestStateMachine {
case .running(.streaming(let expectedBodyLength, let sentBodyBytes, producer: .paused), let responseState):
let startProducing = self.isChannelWritable && expectedBodyLength != sentBodyBytes
self.state = .running(.streaming(
expectedBodyLength: expectedBodyLength,
sentBodyBytes: sentBodyBytes,
producer: startProducing ? .producing : .paused
), responseState)
self.state = .running(
.streaming(
expectedBodyLength: expectedBodyLength,
sentBodyBytes: sentBodyBytes,
producer: startProducing ? .producing : .paused
),
responseState
)
return .notifyRequestHeadSendSuccessfully(
resumeRequestBodyStream: startProducing,
startIdleTimer: false
@@ -757,7 +811,9 @@ struct HTTPRequestStateMachine {
case .running(.endSent, _):
return .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
case .running(.streaming(_, _, producer: .producing), _):
preconditionFailure("request body producing can not start before we have successfully send the header \(self.state)")
preconditionFailure(
"request body producing can not start before we have successfully send the header \(self.state)"
)
case .failed:
return .wait
@@ -830,7 +886,8 @@ extension HTTPRequestStateMachine: CustomStringConvertible {
case .waitForChannelToBecomeWritable:
return "HTTPRequestStateMachine(.waitForChannelToBecomeWritable, isWritable: \(self.isChannelWritable))"
case .running(let requestState, let responseState):
return "HTTPRequestStateMachine(.running(request: \(requestState), response: \(responseState)), isWritable: \(self.isChannelWritable))"
return
"HTTPRequestStateMachine(.running(request: \(requestState), response: \(responseState)), isWritable: \(self.isChannelWritable))"
case .finished:
return "HTTPRequestStateMachine(.finished, isWritable: \(self.isChannelWritable))"
case .failed(let error):
@@ -844,7 +901,7 @@ extension HTTPRequestStateMachine: CustomStringConvertible {
extension HTTPRequestStateMachine.RequestState: CustomStringConvertible {
var description: String {
switch self {
case .streaming(expectedBodyLength: let expected, let sent, producer: let producer):
case .streaming(expectedBodyLength: let expected, let sent, let producer):
return ".streaming(sent: \(expected != nil ? String(expected!) : "-"), sent: \(sent), producer: \(producer)"
case .endSent:
return ".endSent"
@@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//
import NIOCore
#if canImport(Darwin)
import func Darwin.pow
#elseif canImport(Musl)
@@ -71,7 +71,7 @@ extension HTTPConnectionPool {
var idleAndNoRemainingUses: Bool {
switch self.state {
case .idle(_, since: _, remainingUses: let remainingUses):
case .idle(_, since: _, let remainingUses):
if let remainingUses = remainingUses {
return remainingUses <= 0
} else {
@@ -139,7 +139,7 @@ extension HTTPConnectionPool {
mutating func lease() -> Connection {
switch self.state {
case .idle(let connection, since: _, remainingUses: let remainingUses):
case .idle(let connection, since: _, let remainingUses):
self.state = .leased(connection, remainingUses: remainingUses.map { $0 - 1 })
return connection
case .backingOff, .starting, .leased, .closed:
@@ -208,7 +208,9 @@ extension HTTPConnectionPool {
context.cancel.append(connection)
return .keepConnection
case .closed:
preconditionFailure("Unexpected state: Did not expect to have connections with this state in the state machine: \(self.state)")
preconditionFailure(
"Unexpected state: Did not expect to have connections with this state in the state machine: \(self.state)"
)
}
}
@@ -232,7 +234,9 @@ extension HTTPConnectionPool {
case .leased:
return .keepConnection
case .closed:
preconditionFailure("Unexpected state: Did not expect to have connections with this state in the state machine: \(self.state)")
preconditionFailure(
"Unexpected state: Did not expect to have connections with this state in the state machine: \(self.state)"
)
}
}
}
@@ -316,7 +320,7 @@ extension HTTPConnectionPool {
}
func startingEventLoopConnections(on eventLoop: EventLoop) -> Int {
return self.connections[self.overflowIndex..<self.connections.endIndex].reduce(into: 0) { count, connection in
self.connections[self.overflowIndex..<self.connections.endIndex].reduce(into: 0) { count, connection in
guard connection.eventLoop === eventLoop else { return }
if connection.isConnecting || connection.isBackingOff {
count &+= 1
@@ -395,7 +399,10 @@ extension HTTPConnectionPool {
guard let index = self.connections.firstIndex(where: { $0.connectionID == connection.id }) else {
preconditionFailure("There is a new connection that we didn't request!")
}
precondition(connection.eventLoop === self.connections[index].eventLoop, "Expected the new connection to be on EL")
precondition(
connection.eventLoop === self.connections[index].eventLoop,
"Expected the new connection to be on EL"
)
self.connections[index].connected(connection)
let context = self.generateIdleConnectionContextForConnection(at: index)
return (index, context)
@@ -646,14 +653,16 @@ extension HTTPConnectionPool {
},
uniquingKeysWith: +
)
var connectionToCreate = requiredEventLoopOfPendingRequests
var connectionToCreate =
requiredEventLoopOfPendingRequests
.flatMap { eventLoop, requestCount -> [(Connection.ID, EventLoop)] in
// We need a connection for each queued request with a required event loop.
// Therefore, we look how many request we have queued for a given `eventLoop` and
// how many connections we are already starting on the given `eventLoop`.
// If we have not enough, we will create additional connections to have at least
// on connection per request.
let connectionsToStart = requestCount - startingRequiredEventLoopConnectionCount[eventLoop.id, default: 0]
let connectionsToStart =
requestCount - startingRequiredEventLoopConnectionCount[eventLoop.id, default: 0]
return stride(from: 0, to: connectionsToStart, by: 1).lazy.map { _ in
(self.createNewOverflowConnection(on: eventLoop), eventLoop)
}
@@ -668,7 +677,8 @@ extension HTTPConnectionPool {
// event loop we will continue with the event loop with the second most queued requests
// and so on and so forth. The `generalPurposeRequestCountGroupedByPreferredEventLoop`
// array is already ordered so we can just iterate over it without sorting by request count.
let newGeneralPurposeConnections: [(Connection.ID, EventLoop)] = generalPurposeRequestCountGroupedByPreferredEventLoop
let newGeneralPurposeConnections: [(Connection.ID, EventLoop)] =
generalPurposeRequestCountGroupedByPreferredEventLoop
// we do not want to allocated intermediate arrays.
.lazy
// we flatten the grouped list of event loops by lazily repeating the event loop
@@ -80,7 +80,10 @@ extension HTTPConnectionPool {
requests: RequestQueue
) -> ConnectionMigrationAction {
precondition(self.connections.isEmpty, "expected an empty state machine but connections are not empty")
precondition(self.http2Connections == nil, "expected an empty state machine but http2Connections are not nil")
precondition(
self.http2Connections == nil,
"expected an empty state machine but http2Connections are not nil"
)
precondition(self.requests.isEmpty, "expected an empty state machine but requests are not empty")
self.requests = requests
@@ -100,7 +103,8 @@ extension HTTPConnectionPool {
let createConnections = self.connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopOfPendingRequests: requests.requestCountGroupedByRequiredEventLoop(),
generalPurposeRequestCountGroupedByPreferredEventLoop: requests.generalPurposeRequestCountGroupedByPreferredEventLoop()
generalPurposeRequestCountGroupedByPreferredEventLoop:
requests.generalPurposeRequestCountGroupedByPreferredEventLoop()
)
if !http2Connections.isEmpty {
@@ -229,7 +233,9 @@ extension HTTPConnectionPool {
case .running:
guard self.retryConnectionEstablishment else {
guard let (index, _) = self.connections.failConnection(connectionID) else {
preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.")
preconditionFailure(
"A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost."
)
}
self.connections.removeConnection(at: index)
@@ -295,7 +301,10 @@ extension HTTPConnectionPool {
return .none
}
precondition(self.lifecycleState == .running, "If we are shutting down, we must not have any idle connections")
precondition(
self.lifecycleState == .running,
"If we are shutting down, we must not have any idle connections"
)
return .init(
request: .none,
@@ -561,7 +570,8 @@ extension HTTPConnectionPool {
// MARK: HTTP2
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action {
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action
{
// The `http2Connections` are optional here:
// Connections report events back to us, if they are in a shutdown that was
// initiated by the state machine. For this reason this callback might be invoked
@@ -663,6 +673,7 @@ extension HTTPConnectionPool.HTTP1StateMachine: CustomStringConvertible {
let stats = self.connections.stats
let queued = self.requests.count
return "connections: [connecting: \(stats.connecting) | backoff: \(stats.backingOff) | leased: \(stats.leased) | idle: \(stats.idle)], queued: \(queued)"
return
"connections: [connecting: \(stats.connecting) | backoff: \(stats.backingOff) | leased: \(stats.leased) | idle: \(stats.idle)], queued: \(queued)"
}
}
@@ -117,7 +117,13 @@ extension HTTPConnectionPool {
preconditionFailure("Invalid state: \(self.state)")
case .starting(let maxUses):
self.state = .active(conn, maxStreams: maxStreams, usedStreams: 0, lastIdle: .now(), remainingUses: maxUses)
self.state = .active(
conn,
maxStreams: maxStreams,
usedStreams: 0,
lastIdle: .now(),
remainingUses: maxUses
)
if let maxUses = maxUses {
return min(maxStreams, maxUses)
} else {
@@ -136,7 +142,13 @@ extension HTTPConnectionPool {
preconditionFailure("Invalid state for updating max concurrent streams: \(self.state)")
case .active(let conn, _, let usedStreams, let lastIdle, let remainingUses):
self.state = .active(conn, maxStreams: maxStreams, usedStreams: usedStreams, lastIdle: lastIdle, remainingUses: remainingUses)
self.state = .active(
conn,
maxStreams: maxStreams,
usedStreams: usedStreams,
lastIdle: lastIdle,
remainingUses: remainingUses
)
let availableStreams = max(maxStreams - usedStreams, 0)
if let remainingUses = remainingUses {
return min(remainingUses, availableStreams)
@@ -192,8 +204,17 @@ extension HTTPConnectionPool {
case .active(let conn, let maxStreams, var usedStreams, let lastIdle, let remainingUses):
usedStreams += count
precondition(usedStreams <= maxStreams, "tried to lease a connection which is not available")
precondition(remainingUses.map { $0 >= count } ?? true, "tried to lease streams from a connection which does not have enough remaining streams")
self.state = .active(conn, maxStreams: maxStreams, usedStreams: usedStreams, lastIdle: lastIdle, remainingUses: remainingUses.map { $0 - count })
precondition(
remainingUses.map { $0 >= count } ?? true,
"tried to lease streams from a connection which does not have enough remaining streams"
)
self.state = .active(
conn,
maxStreams: maxStreams,
usedStreams: usedStreams,
lastIdle: lastIdle,
remainingUses: remainingUses.map { $0 - count }
)
return conn
}
}
@@ -212,7 +233,13 @@ extension HTTPConnectionPool {
lastIdle = .now()
}
self.state = .active(conn, maxStreams: maxStreams, usedStreams: usedStreams, lastIdle: lastIdle, remainingUses: remainingUses)
self.state = .active(
conn,
maxStreams: maxStreams,
usedStreams: usedStreams,
lastIdle: lastIdle,
remainingUses: remainingUses
)
let availableStreams = max(maxStreams &- usedStreams, 0)
if let remainingUses = remainingUses {
return min(availableStreams, remainingUses)
@@ -282,7 +309,9 @@ extension HTTPConnectionPool {
return .keepConnection
case .closed:
preconditionFailure("Unexpected state for cleanup: Did not expect to have closed connections in the state machine.")
preconditionFailure(
"Unexpected state for cleanup: Did not expect to have closed connections in the state machine."
)
}
}
@@ -341,7 +370,9 @@ extension HTTPConnectionPool {
return .removeConnection
case .closed:
preconditionFailure("Unexpected state: Did not expect to have connections with this state in the state machine: \(self.state)")
preconditionFailure(
"Unexpected state: Did not expect to have connections with this state in the state machine: \(self.state)"
)
}
}
@@ -388,16 +419,20 @@ extension HTTPConnectionPool {
backingOff: [(Connection.ID, EventLoop)]
) {
for (connectionID, eventLoop) in starting {
let newConnection = HTTP2ConnectionState(connectionID: connectionID,
eventLoop: eventLoop,
maximumUses: self.maximumConnectionUses)
let newConnection = HTTP2ConnectionState(
connectionID: connectionID,
eventLoop: eventLoop,
maximumUses: self.maximumConnectionUses
)
self.connections.append(newConnection)
}
for (connectionID, eventLoop) in backingOff {
var backingOffConnection = HTTP2ConnectionState(connectionID: connectionID,
eventLoop: eventLoop,
maximumUses: self.maximumConnectionUses)
var backingOffConnection = HTTP2ConnectionState(
connectionID: connectionID,
eventLoop: eventLoop,
maximumUses: self.maximumConnectionUses
)
// TODO: Maybe we want to add a static init for backing off connections to HTTP2ConnectionState
backingOffConnection.failedToConnect()
self.connections.append(backingOffConnection)
@@ -503,9 +538,11 @@ extension HTTPConnectionPool {
"we should not create more than one connection per event loop"
)
let connection = HTTP2ConnectionState(connectionID: self.generator.next(),
eventLoop: eventLoop,
maximumUses: self.maximumConnectionUses)
let connection = HTTP2ConnectionState(
connectionID: self.generator.next(),
eventLoop: eventLoop,
maximumUses: self.maximumConnectionUses
)
self.connections.append(connection)
return connection.connectionID
}
@@ -518,11 +555,17 @@ extension HTTPConnectionPool {
/// - Returns: An index and an ``EstablishedConnectionContext`` to determine the next action for the now idle connection.
/// Call ``leaseStreams(at:count:)`` or ``closeConnection(at:)`` with the supplied index after
/// this.
mutating func newHTTP2ConnectionEstablished(_ connection: Connection, maxConcurrentStreams: Int) -> (Int, EstablishedConnectionContext) {
mutating func newHTTP2ConnectionEstablished(
_ connection: Connection,
maxConcurrentStreams: Int
) -> (Int, EstablishedConnectionContext) {
guard let index = self.connections.firstIndex(where: { $0.connectionID == connection.id }) else {
preconditionFailure("There is a new connection that we didn't request!")
}
precondition(connection.eventLoop === self.connections[index].eventLoop, "Expected the new connection to be on EL")
precondition(
connection.eventLoop === self.connections[index].eventLoop,
"Expected the new connection to be on EL"
)
let availableStreams = self.connections[index].connected(connection, maxStreams: maxConcurrentStreams)
let context = EstablishedConnectionContext(
availableStreams: availableStreams,
@@ -47,8 +47,10 @@ extension HTTPConnectionPool {
self.idGenerator = idGenerator
self.requests = RequestQueue()
self.connections = HTTP2Connections(generator: idGenerator,
maximumConnectionUses: maximumConnectionUses)
self.connections = HTTP2Connections(
generator: idGenerator,
maximumConnectionUses: maximumConnectionUses
)
self.lifecycleState = lifecycleState
self.retryConnectionEstablishment = retryConnectionEstablishment
}
@@ -83,7 +85,10 @@ extension HTTPConnectionPool {
requests: RequestQueue
) -> ConnectionMigrationAction {
precondition(self.connections.isEmpty, "expected an empty state machine but connections are not empty")
precondition(self.http1Connections == nil, "expected an empty state machine but http1Connections are not nil")
precondition(
self.http1Connections == nil,
"expected an empty state machine but http1Connections are not nil"
)
precondition(self.requests.isEmpty, "expected an empty state machine but requests are not empty")
self.requests = requests
@@ -93,7 +98,7 @@ extension HTTPConnectionPool {
self.connections = http2Connections
}
var http1Connections = http1Connections // make http1Connections mutable
var http1Connections = http1Connections // make http1Connections mutable
let context = http1Connections.migrateToHTTP2()
self.connections.migrateFromHTTP1(
starting: context.starting,
@@ -215,7 +220,10 @@ extension HTTPConnectionPool {
.init(self._newHTTP2ConnectionEstablished(connection, maxConcurrentStreams: maxConcurrentStreams))
}
private mutating func _newHTTP2ConnectionEstablished(_ connection: Connection, maxConcurrentStreams: Int) -> EstablishedAction {
private mutating func _newHTTP2ConnectionEstablished(
_ connection: Connection,
maxConcurrentStreams: Int
) -> EstablishedAction {
self.failedConsecutiveConnectionAttempts = 0
self.lastConnectFailure = nil
if self.connections.hasActiveConnection(for: connection.eventLoop) {
@@ -296,8 +304,14 @@ extension HTTPConnectionPool {
}
}
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action {
guard let (index, context) = self.connections.newHTTP2MaxConcurrentStreamsReceived(connectionID, newMaxStreams: newMaxStreams) else {
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action
{
guard
let (index, context) = self.connections.newHTTP2MaxConcurrentStreamsReceived(
connectionID,
newMaxStreams: newMaxStreams
)
else {
// When a connection close is initiated by the connection pool, the connection will
// still report further events (like newMaxConcurrentStreamsReceived) to the state
// machine. In those cases we must ignore the event.
@@ -341,15 +355,15 @@ extension HTTPConnectionPool {
// we need to start a new on connection in two cases:
let needGeneralPurposeConnection =
// 1. if we have general purpose requests
!self.requests.isEmpty(for: nil) &&
!self.requests.isEmpty(for: nil)
// and no connection starting or active
!context.hasGeneralPurposeConnection
&& !context.hasGeneralPurposeConnection
let needRequiredEventLoopConnection =
// 2. or if we have requests for a required event loop
!self.requests.isEmpty(for: eventLoop) &&
!self.requests.isEmpty(for: eventLoop)
// and no connection starting or active for the given event loop
!context.hasConnectionOnSpecifiedEventLoop
&& !context.hasConnectionOnSpecifiedEventLoop
guard needGeneralPurposeConnection || needRequiredEventLoopConnection else {
// otherwise we can remove the connection
@@ -357,7 +371,8 @@ extension HTTPConnectionPool {
return .none
}
let (newConnectionID, previousEventLoop) = self.connections.createNewConnectionByReplacingClosedConnection(at: index)
let (newConnectionID, previousEventLoop) = self.connections
.createNewConnectionByReplacingClosedConnection(at: index)
precondition(previousEventLoop === eventLoop)
return .init(
@@ -413,7 +428,9 @@ extension HTTPConnectionPool {
case .running:
guard self.retryConnectionEstablishment else {
guard let (index, _) = self.connections.failConnection(connectionID) else {
preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.")
preconditionFailure(
"A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost."
)
}
self.connections.removeConnection(at: index)
@@ -425,10 +442,15 @@ extension HTTPConnectionPool {
let eventLoop = self.connections.backoffNextConnectionAttempt(connectionID)
let backoff = calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts)
return .init(request: .none, connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop))
return .init(
request: .none,
connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop)
)
case .shuttingDown:
guard let (index, context) = self.connections.failConnection(connectionID) else {
preconditionFailure("A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost.")
preconditionFailure(
"A connection attempt failed, that the state machine knows nothing about. Somewhere state was lost."
)
}
return self.nextActionForFailedConnection(at: index, on: context.eventLoop)
case .shutDown:
@@ -505,7 +527,10 @@ extension HTTPConnectionPool {
return .none
}
precondition(self.lifecycleState == .running, "If we are shutting down, we must not have any idle connections")
precondition(
self.lifecycleState == .running,
"If we are shutting down, we must not have any idle connections"
)
return .init(
request: .none,
@@ -558,7 +583,10 @@ extension HTTPConnectionPool {
case .shuttingDown(let unclean):
if self.connections.isEmpty {
// if the http2connections are empty as well, there are no more connections. Shutdown completed.
return .init(request: .none, connection: .closeConnection(connection, isShutdown: .yes(unclean: unclean)))
return .init(
request: .none,
connection: .closeConnection(connection, isShutdown: .yes(unclean: unclean))
)
} else {
return .init(request: .none, connection: .closeConnection(connection, isShutdown: .no))
}
@@ -134,11 +134,14 @@ extension HTTPConnectionPool {
}
mutating func executeRequest(_ request: Request) -> Action {
self.state.modify(http1: { http1 in
http1.executeRequest(request)
}, http2: { http2 in
http2.executeRequest(request)
})
self.state.modify(
http1: { http1 in
http1.executeRequest(request)
},
http2: { http2 in
http2.executeRequest(request)
}
)
}
mutating func newHTTP1ConnectionCreated(_ connection: Connection) -> Action {
@@ -199,60 +202,82 @@ extension HTTPConnectionPool {
}
}
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action {
self.state.modify(http1: { http1 in
http1.newHTTP2MaxConcurrentStreamsReceived(connectionID, newMaxStreams: newMaxStreams)
}, http2: { http2 in
http2.newHTTP2MaxConcurrentStreamsReceived(connectionID, newMaxStreams: newMaxStreams)
})
mutating func newHTTP2MaxConcurrentStreamsReceived(_ connectionID: Connection.ID, newMaxStreams: Int) -> Action
{
self.state.modify(
http1: { http1 in
http1.newHTTP2MaxConcurrentStreamsReceived(connectionID, newMaxStreams: newMaxStreams)
},
http2: { http2 in
http2.newHTTP2MaxConcurrentStreamsReceived(connectionID, newMaxStreams: newMaxStreams)
}
)
}
mutating func http2ConnectionGoAwayReceived(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.http2ConnectionGoAwayReceived(connectionID)
}, http2: { http2 in
http2.http2ConnectionGoAwayReceived(connectionID)
})
self.state.modify(
http1: { http1 in
http1.http2ConnectionGoAwayReceived(connectionID)
},
http2: { http2 in
http2.http2ConnectionGoAwayReceived(connectionID)
}
)
}
mutating func http2ConnectionClosed(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.http2ConnectionClosed(connectionID)
}, http2: { http2 in
http2.http2ConnectionClosed(connectionID)
})
self.state.modify(
http1: { http1 in
http1.http2ConnectionClosed(connectionID)
},
http2: { http2 in
http2.http2ConnectionClosed(connectionID)
}
)
}
mutating func http2ConnectionStreamClosed(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.http2ConnectionStreamClosed(connectionID)
}, http2: { http2 in
http2.http2ConnectionStreamClosed(connectionID)
})
self.state.modify(
http1: { http1 in
http1.http2ConnectionStreamClosed(connectionID)
},
http2: { http2 in
http2.http2ConnectionStreamClosed(connectionID)
}
)
}
mutating func failedToCreateNewConnection(_ error: Error, connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.failedToCreateNewConnection(error, connectionID: connectionID)
}, http2: { http2 in
http2.failedToCreateNewConnection(error, connectionID: connectionID)
})
self.state.modify(
http1: { http1 in
http1.failedToCreateNewConnection(error, connectionID: connectionID)
},
http2: { http2 in
http2.failedToCreateNewConnection(error, connectionID: connectionID)
}
)
}
mutating func waitingForConnectivity(_ error: Error, connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.waitingForConnectivity(error, connectionID: connectionID)
}, http2: { http2 in
http2.waitingForConnectivity(error, connectionID: connectionID)
})
self.state.modify(
http1: { http1 in
http1.waitingForConnectivity(error, connectionID: connectionID)
},
http2: { http2 in
http2.waitingForConnectivity(error, connectionID: connectionID)
}
)
}
mutating func connectionCreationBackoffDone(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.connectionCreationBackoffDone(connectionID)
}, http2: { http2 in
http2.connectionCreationBackoffDone(connectionID)
})
self.state.modify(
http1: { http1 in
http1.connectionCreationBackoffDone(connectionID)
},
http2: { http2 in
http2.connectionCreationBackoffDone(connectionID)
}
)
}
/// A request has timed out.
@@ -261,11 +286,14 @@ extension HTTPConnectionPool {
/// request, but don't need to cancel the timer (it already triggered). If a request is cancelled
/// we don't need to fail it but we need to cancel its timeout timer.
mutating func timeoutRequest(_ requestID: Request.ID) -> Action {
self.state.modify(http1: { http1 in
http1.timeoutRequest(requestID)
}, http2: { http2 in
http2.timeoutRequest(requestID)
})
self.state.modify(
http1: { http1 in
http1.timeoutRequest(requestID)
},
http2: { http2 in
http2.timeoutRequest(requestID)
}
)
}
/// A request was cancelled.
@@ -274,44 +302,59 @@ extension HTTPConnectionPool {
/// need to cancel its timeout timer. If a request times out, we need to fail the request, but don't
/// need to cancel the timer (it already triggered).
mutating func cancelRequest(_ requestID: Request.ID) -> Action {
self.state.modify(http1: { http1 in
http1.cancelRequest(requestID)
}, http2: { http2 in
http2.cancelRequest(requestID)
})
self.state.modify(
http1: { http1 in
http1.cancelRequest(requestID)
},
http2: { http2 in
http2.cancelRequest(requestID)
}
)
}
mutating func connectionIdleTimeout(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.connectionIdleTimeout(connectionID)
}, http2: { http2 in
http2.connectionIdleTimeout(connectionID)
})
self.state.modify(
http1: { http1 in
http1.connectionIdleTimeout(connectionID)
},
http2: { http2 in
http2.connectionIdleTimeout(connectionID)
}
)
}
/// A connection has been closed
mutating func http1ConnectionClosed(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.http1ConnectionClosed(connectionID)
}, http2: { http2 in
http2.http1ConnectionClosed(connectionID)
})
self.state.modify(
http1: { http1 in
http1.http1ConnectionClosed(connectionID)
},
http2: { http2 in
http2.http1ConnectionClosed(connectionID)
}
)
}
mutating func http1ConnectionReleased(_ connectionID: Connection.ID) -> Action {
self.state.modify(http1: { http1 in
http1.http1ConnectionReleased(connectionID)
}, http2: { http2 in
http2.http1ConnectionReleased(connectionID)
})
self.state.modify(
http1: { http1 in
http1.http1ConnectionReleased(connectionID)
},
http2: { http2 in
http2.http1ConnectionReleased(connectionID)
}
)
}
mutating func shutdown() -> Action {
return self.state.modify(http1: { http1 in
http1.shutdown()
}, http2: { http2 in
http2.shutdown()
})
self.state.modify(
http1: { http1 in
http1.shutdown()
},
http2: { http2 in
http2.shutdown()
}
)
}
}
}
@@ -362,7 +405,10 @@ extension HTTPConnectionPool.StateMachine {
enum EstablishedConnectionAction {
case none
case scheduleTimeoutTimer(HTTPConnectionPool.Connection.ID, on: EventLoop)
case closeConnection(HTTPConnectionPool.Connection, isShutdown: HTTPConnectionPool.StateMachine.ConnectionAction.IsShutdown)
case closeConnection(
HTTPConnectionPool.Connection,
isShutdown: HTTPConnectionPool.StateMachine.ConnectionAction.IsShutdown
)
}
}
@@ -403,8 +449,7 @@ extension HTTPConnectionPool.StateMachine.ConnectionAction {
case .closeConnection(let connection, let isShutdown):
guard isShutdown == .no else {
precondition(
migrationAction.closeConnections.isEmpty &&
migrationAction.createConnections.isEmpty,
migrationAction.closeConnections.isEmpty && migrationAction.createConnections.isEmpty,
"migration actions are not supported during shutdown"
)
return .closeConnection(connection, isShutdown: isShutdown)
@@ -87,12 +87,12 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate {
path: path,
pool: .some(pool),
reportHead: reportHead.map { reportHead in
return { _, head in
{ _, head in
reportHead(head)
}
},
reportProgress: reportProgress.map { reportProgress in
return { _, head in
{ _, head in
reportProgress(head)
}
}
@@ -117,12 +117,12 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate {
path: path,
pool: nil,
reportHead: reportHead.map { reportHead in
return { _, head in
{ _, head in
reportHead(head)
}
},
reportProgress: reportProgress.map { reportProgress in
return { _, head in
{ _, head in
reportProgress(head)
}
}
@@ -136,7 +136,8 @@ public final class FileDownloadDelegate: HTTPClientResponseDelegate {
self.reportHead?(task, head)
if let totalBytesString = head.headers.first(name: "Content-Length"),
let totalBytes = Int(totalBytesString) {
let totalBytes = Int(totalBytesString)
{
self.progress.totalBytes = totalBytes
}
@@ -39,7 +39,16 @@ extension HTTPClient.Cookie {
/// - maxAge: The cookie's age in seconds, defaults to nil.
/// - httpOnly: Whether this cookie should be used by HTTP servers only, defaults to false.
/// - secure: Whether this cookie should only be sent using secure channels, defaults to false.
public init(name: String, value: String, path: String = "/", domain: String? = nil, expires: Date? = nil, maxAge: Int? = nil, httpOnly: Bool = false, secure: Bool = false) {
public init(
name: String,
value: String,
path: String = "/",
domain: String? = nil,
expires: Date? = nil,
maxAge: Int? = nil,
httpOnly: Bool = false,
secure: Bool = false
) {
// FIXME: This should be failable and validate the inputs
// (for example, checking that the strings are ASCII, path begins with "/", domain is not empty, etc).
self.init(
@@ -59,8 +68,8 @@ extension HTTPClient.Body {
/// Create and stream body using `Data`.
///
/// - parameters:
/// - bytes: Body `Data` representation.
/// - data: Body `Data` representation.
public static func data(_ data: Data) -> HTTPClient.Body {
return self.bytes(data)
self.bytes(data)
}
}
@@ -12,7 +12,10 @@
//
//===----------------------------------------------------------------------===//
import CAsyncHTTPClient
import NIOCore
import NIOHTTP1
#if canImport(xlocale)
import xlocale
#elseif canImport(locale_h)
@@ -27,9 +30,6 @@ import Musl
import Glibc
#endif
import CAsyncHTTPClient
import NIOCore
extension HTTPClient {
/// A representation of an HTTP cookie.
public struct Cookie: Sendable {
@@ -55,7 +55,6 @@ extension HTTPClient {
/// - parameters:
/// - header: String representation of the `Set-Cookie` response header.
/// - defaultDomain: Default domain to use if cookie was sent without one.
/// - returns: nil if the header is invalid.
public init?(header: String, defaultDomain: String) {
// The parsing of "Set-Cookie" headers is defined by Section 5.2, RFC-6265:
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2
@@ -136,7 +135,16 @@ extension HTTPClient {
/// - maxAge: The cookie's age in seconds, defaults to nil.
/// - httpOnly: Whether this cookie should be used by HTTP servers only, defaults to false.
/// - secure: Whether this cookie should only be sent using secure channels, defaults to false.
internal init(name: String, value: String, path: String = "/", domain: String? = nil, expires_timestamp: Int64? = nil, maxAge: Int? = nil, httpOnly: Bool = false, secure: Bool = false) {
internal init(
name: String,
value: String,
path: String = "/",
domain: String? = nil,
expires_timestamp: Int64? = nil,
maxAge: Int? = nil,
httpOnly: Bool = false,
secure: Bool = false
) {
self.name = name
self.value = value
self.path = path
@@ -152,7 +160,7 @@ extension HTTPClient {
extension HTTPClient.Response {
/// List of HTTP cookies returned by the server.
public var cookies: [HTTPClient.Cookie] {
return self.headers["set-cookie"].compactMap { HTTPClient.Cookie(header: $0, defaultDomain: self.host) }
self.headers["set-cookie"].compactMap { HTTPClient.Cookie(header: $0, defaultDomain: self.host) }
}
}
@@ -222,7 +230,8 @@ private func parseTimestamp(_ utf8: String.UTF8View.SubSequence, format: String)
}
private func parseCookieTime(_ timestampUTF8: String.UTF8View.SubSequence) -> Int64? {
if timestampUTF8.contains(where: { $0 < 0x20 /* Control characters */ || $0 == 0x7F /* DEL */ }) {
// 0x20: Control characters or 0x7F: DEL
if timestampUTF8.contains(where: { $0 < 0x20 || $0 == 0x7F }) {
return nil
}
var timestampUTF8 = timestampUTF8
@@ -235,8 +244,8 @@ private func parseCookieTime(_ timestampUTF8: String.UTF8View.SubSequence) -> In
}
guard
var timeComponents = parseTimestamp(timestampUTF8, format: "%a, %d %b %Y %H:%M:%S")
?? parseTimestamp(timestampUTF8, format: "%a, %d-%b-%y %H:%M:%S")
?? parseTimestamp(timestampUTF8, format: "%a %b %d %H:%M:%S %Y")
?? parseTimestamp(timestampUTF8, format: "%a, %d-%b-%y %H:%M:%S")
?? parseTimestamp(timestampUTF8, format: "%a %b %d %H:%M:%S %Y")
else {
return nil
}
@@ -38,7 +38,10 @@ extension HTTPClient.Configuration {
/// Specifies Proxy server authorization.
public var authorization: HTTPClient.Authorization? {
set {
precondition(self.type == .http(self.authorization), "SOCKS authorization support is not yet implemented.")
precondition(
self.type == .http(self.authorization),
"SOCKS authorization support is not yet implemented."
)
self.type = .http(newValue)
}
@@ -60,7 +63,7 @@ extension HTTPClient.Configuration {
/// - host: proxy server host.
/// - port: proxy server port.
public static func server(host: String, port: Int) -> Proxy {
return .init(host: host, port: port, type: .http(nil))
.init(host: host, port: port, type: .http(nil))
}
/// Create a HTTP proxy.
@@ -70,7 +73,7 @@ extension HTTPClient.Configuration {
/// - port: proxy server port.
/// - authorization: proxy server authorization.
public static func server(host: String, port: Int, authorization: HTTPClient.Authorization? = nil) -> Proxy {
return .init(host: host, port: port, type: .http(authorization))
.init(host: host, port: port, type: .http(authorization))
}
/// Create a SOCKSv5 proxy.
@@ -78,7 +81,7 @@ extension HTTPClient.Configuration {
/// - parameter port: The SOCKSv5 proxy port, defaults to 1080.
/// - returns: A new instance of `Proxy` configured to connect to a `SOCKSv5` server.
public static func socksServer(host: String, port: Int = 1080) -> Proxy {
return .init(host: host, port: port, type: .socks)
.init(host: host, port: port, type: .socks)
}
}
}
+290 -159
View File
@@ -26,7 +26,7 @@ import NIOTransportServices
extension Logger {
private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value {
return "\(request.method) \(request.url)"
"\(request.method) \(request.url)"
}
func attachingRequestInformation(_ request: HTTPClient.Request, requestID: Int) -> Logger {
@@ -80,23 +80,31 @@ public class HTTPClient {
/// - parameters:
/// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created.
/// - configuration: Client configuration.
public convenience init(eventLoopGroupProvider: EventLoopGroupProvider,
configuration: Configuration = Configuration()) {
self.init(eventLoopGroupProvider: eventLoopGroupProvider,
configuration: configuration,
backgroundActivityLogger: HTTPClient.loggingDisabled)
public convenience init(
eventLoopGroupProvider: EventLoopGroupProvider,
configuration: Configuration = Configuration()
) {
self.init(
eventLoopGroupProvider: eventLoopGroupProvider,
configuration: configuration,
backgroundActivityLogger: HTTPClient.loggingDisabled
)
}
/// Create an ``HTTPClient`` with specified `EventLoopGroup` and configuration.
///
/// - parameters:
/// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created.
/// - eventLoopGroup: Specify how `EventLoopGroup` will be created.
/// - configuration: Client configuration.
public convenience init(eventLoopGroup: EventLoopGroup = HTTPClient.defaultEventLoopGroup,
configuration: Configuration = Configuration()) {
self.init(eventLoopGroupProvider: .shared(eventLoopGroup),
configuration: configuration,
backgroundActivityLogger: HTTPClient.loggingDisabled)
public convenience init(
eventLoopGroup: EventLoopGroup = HTTPClient.defaultEventLoopGroup,
configuration: Configuration = Configuration()
) {
self.init(
eventLoopGroupProvider: .shared(eventLoopGroup),
configuration: configuration,
backgroundActivityLogger: HTTPClient.loggingDisabled
)
}
/// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration.
@@ -104,21 +112,26 @@ public class HTTPClient {
/// - parameters:
/// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created.
/// - configuration: Client configuration.
public convenience init(eventLoopGroupProvider: EventLoopGroupProvider,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger) {
/// - backgroundActivityLogger: The logger to use for background activity logs.
public convenience init(
eventLoopGroupProvider: EventLoopGroupProvider,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger
) {
let eventLoopGroup: any EventLoopGroup
switch eventLoopGroupProvider {
case .shared(let group):
eventLoopGroup = group
default: // handle `.createNew` without a deprecation warning
default: // handle `.createNew` without a deprecation warning
eventLoopGroup = HTTPClient.defaultEventLoopGroup
}
self.init(eventLoopGroup: eventLoopGroup,
configuration: configuration,
backgroundActivityLogger: backgroundActivityLogger)
self.init(
eventLoopGroup: eventLoopGroup,
configuration: configuration,
backgroundActivityLogger: backgroundActivityLogger
)
}
/// Create an ``HTTPClient`` with specified `EventLoopGroup` and configuration.
@@ -127,19 +140,25 @@ public class HTTPClient {
/// - eventLoopGroup: The `EventLoopGroup` that the ``HTTPClient`` will use.
/// - configuration: Client configuration.
/// - backgroundActivityLogger: The `Logger` that will be used to log background any activity that's not associated with a request.
public convenience init(eventLoopGroup: any EventLoopGroup = HTTPClient.defaultEventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger) {
self.init(eventLoopGroup: eventLoopGroup,
configuration: configuration,
backgroundActivityLogger: backgroundActivityLogger,
canBeShutDown: true)
public convenience init(
eventLoopGroup: any EventLoopGroup = HTTPClient.defaultEventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger
) {
self.init(
eventLoopGroup: eventLoopGroup,
configuration: configuration,
backgroundActivityLogger: backgroundActivityLogger,
canBeShutDown: true
)
}
internal required init(eventLoopGroup: EventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger,
canBeShutDown: Bool) {
internal required init(
eventLoopGroup: EventLoopGroup,
configuration: Configuration = Configuration(),
backgroundActivityLogger: Logger,
canBeShutDown: Bool
) {
self.canBeShutDown = canBeShutDown
self.eventLoopGroup = eventLoopGroup
self.configuration = configuration
@@ -158,15 +177,19 @@ public class HTTPClient {
case .shutDown:
break
case .shuttingDown:
preconditionFailure("""
This state should be totally unreachable. While the HTTPClient is shutting down a \
reference cycle should exist, that prevents it from deinit.
""")
preconditionFailure(
"""
This state should be totally unreachable. While the HTTPClient is shutting down a \
reference cycle should exist, that prevents it from deinit.
"""
)
case .upAndRunning:
preconditionFailure("""
Client not shut down before the deinit. Please call client.shutdown() when no \
longer needed. Otherwise memory will leak.
""")
preconditionFailure(
"""
Client not shut down before the deinit. Please call client.shutdown() when no \
longer needed. Otherwise memory will leak.
"""
)
}
}
}
@@ -191,16 +214,19 @@ public class HTTPClient {
/// In general, setting this parameter to `true` should make it easier and faster to catch related programming errors.
func syncShutdown(requiresCleanClose: Bool) throws {
if let eventLoop = MultiThreadedEventLoopGroup.currentEventLoop {
preconditionFailure("""
BUG DETECTED: syncShutdown() must not be called when on an EventLoop.
Calling syncShutdown() on any EventLoop can lead to deadlocks.
Current eventLoop: \(eventLoop)
""")
preconditionFailure(
"""
BUG DETECTED: syncShutdown() must not be called when on an EventLoop.
Calling syncShutdown() on any EventLoop can lead to deadlocks.
Current eventLoop: \(eventLoop)
"""
)
}
let errorStorageLock = NIOLock()
let errorStorage: UnsafeMutableTransferBox<Error?> = .init(nil)
let continuation = DispatchWorkItem {}
self.shutdown(requiresCleanClose: requiresCleanClose, queue: DispatchQueue(label: "async-http-client.shutdown")) { error in
self.shutdown(requiresCleanClose: requiresCleanClose, queue: DispatchQueue(label: "async-http-client.shutdown"))
{ error in
if let error = error {
errorStorageLock.withLock {
errorStorage.wrappedValue = error
@@ -301,7 +327,7 @@ public class HTTPClient {
/// - url: Remote URL.
/// - deadline: Point in time by which the request must complete.
public func get(url: String, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.get(url: url, deadline: deadline, logger: HTTPClient.loggingDisabled)
self.get(url: url, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute `GET` request using specified URL.
@@ -311,7 +337,7 @@ public class HTTPClient {
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func get(url: String, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
return self.execute(.GET, url: url, deadline: deadline, logger: logger)
self.execute(.GET, url: url, deadline: deadline, logger: logger)
}
/// Execute `POST` request using specified URL.
@@ -321,7 +347,7 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
public func post(url: String, body: Body? = nil, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.post(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
self.post(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute `POST` request using specified URL.
@@ -331,8 +357,13 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func post(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
return self.execute(.POST, url: url, body: body, deadline: deadline, logger: logger)
public func post(
url: String,
body: Body? = nil,
deadline: NIODeadline? = nil,
logger: Logger
) -> EventLoopFuture<Response> {
self.execute(.POST, url: url, body: body, deadline: deadline, logger: logger)
}
/// Execute `PATCH` request using specified URL.
@@ -342,7 +373,7 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
public func patch(url: String, body: Body? = nil, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.patch(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
self.patch(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute `PATCH` request using specified URL.
@@ -352,8 +383,13 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func patch(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
return self.execute(.PATCH, url: url, body: body, deadline: deadline, logger: logger)
public func patch(
url: String,
body: Body? = nil,
deadline: NIODeadline? = nil,
logger: Logger
) -> EventLoopFuture<Response> {
self.execute(.PATCH, url: url, body: body, deadline: deadline, logger: logger)
}
/// Execute `PUT` request using specified URL.
@@ -363,7 +399,7 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
public func put(url: String, body: Body? = nil, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.put(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
self.put(url: url, body: body, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute `PUT` request using specified URL.
@@ -373,8 +409,13 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func put(url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
return self.execute(.PUT, url: url, body: body, deadline: deadline, logger: logger)
public func put(
url: String,
body: Body? = nil,
deadline: NIODeadline? = nil,
logger: Logger
) -> EventLoopFuture<Response> {
self.execute(.PUT, url: url, body: body, deadline: deadline, logger: logger)
}
/// Execute `DELETE` request using specified URL.
@@ -383,7 +424,7 @@ public class HTTPClient {
/// - url: Remote URL.
/// - deadline: The time when the request must have been completed by.
public func delete(url: String, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.delete(url: url, deadline: deadline, logger: HTTPClient.loggingDisabled)
self.delete(url: url, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute `DELETE` request using specified URL.
@@ -393,7 +434,7 @@ public class HTTPClient {
/// - deadline: The time when the request must have been completed by.
/// - logger: The logger to use for this request.
public func delete(url: String, deadline: NIODeadline? = nil, logger: Logger) -> EventLoopFuture<Response> {
return self.execute(.DELETE, url: url, deadline: deadline, logger: logger)
self.execute(.DELETE, url: url, deadline: deadline, logger: logger)
}
/// Execute arbitrary HTTP request using specified URL.
@@ -404,7 +445,13 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute(_ method: HTTPMethod = .GET, url: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture<Response> {
public func execute(
_ method: HTTPMethod = .GET,
url: String,
body: Body? = nil,
deadline: NIODeadline? = nil,
logger: Logger? = nil
) -> EventLoopFuture<Response> {
do {
let request = try Request(url: url, method: method, body: body)
return self.execute(request: request, deadline: deadline, logger: logger ?? HTTPClient.loggingDisabled)
@@ -422,7 +469,14 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute(_ method: HTTPMethod = .GET, socketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture<Response> {
public func execute(
_ method: HTTPMethod = .GET,
socketPath: String,
urlPath: String,
body: Body? = nil,
deadline: NIODeadline? = nil,
logger: Logger? = nil
) -> EventLoopFuture<Response> {
do {
guard let url = URL(httpURLWithSocketPath: socketPath, uri: urlPath) else {
throw HTTPClientError.invalidURL
@@ -443,7 +497,14 @@ public class HTTPClient {
/// - body: Request body.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute(_ method: HTTPMethod = .GET, secureSocketPath: String, urlPath: String, body: Body? = nil, deadline: NIODeadline? = nil, logger: Logger? = nil) -> EventLoopFuture<Response> {
public func execute(
_ method: HTTPMethod = .GET,
secureSocketPath: String,
urlPath: String,
body: Body? = nil,
deadline: NIODeadline? = nil,
logger: Logger? = nil
) -> EventLoopFuture<Response> {
do {
guard let url = URL(httpsURLWithSocketPath: secureSocketPath, uri: urlPath) else {
throw HTTPClientError.invalidURL
@@ -461,7 +522,7 @@ public class HTTPClient {
/// - request: HTTP request to execute.
/// - deadline: Point in time by which the request must complete.
public func execute(request: Request, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.execute(request: request, deadline: deadline, logger: HTTPClient.loggingDisabled)
self.execute(request: request, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute arbitrary HTTP request using specified URL.
@@ -481,26 +542,40 @@ public class HTTPClient {
/// - request: HTTP request to execute.
/// - eventLoop: NIO Event Loop preference.
/// - deadline: Point in time by which the request must complete.
public func execute(request: Request, eventLoop: EventLoopPreference, deadline: NIODeadline? = nil) -> EventLoopFuture<Response> {
return self.execute(request: request,
eventLoop: eventLoop,
deadline: deadline,
logger: HTTPClient.loggingDisabled)
public func execute(
request: Request,
eventLoop: EventLoopPreference,
deadline: NIODeadline? = nil
) -> EventLoopFuture<Response> {
self.execute(
request: request,
eventLoop: eventLoop,
deadline: deadline,
logger: HTTPClient.loggingDisabled
)
}
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
///
/// - parameters:
/// - request: HTTP request to execute.
/// - eventLoop: NIO Event Loop preference.
/// - eventLoopPreference: NIO Event Loop preference.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute(request: Request,
eventLoop eventLoopPreference: EventLoopPreference,
deadline: NIODeadline? = nil,
logger: Logger?) -> EventLoopFuture<Response> {
public func execute(
request: Request,
eventLoop eventLoopPreference: EventLoopPreference,
deadline: NIODeadline? = nil,
logger: Logger?
) -> EventLoopFuture<Response> {
let accumulator = ResponseAccumulator(request: request)
return self.execute(request: request, delegate: accumulator, eventLoop: eventLoopPreference, deadline: deadline, logger: logger).futureResult
return self.execute(
request: request,
delegate: accumulator,
eventLoop: eventLoopPreference,
deadline: deadline,
logger: logger
).futureResult
}
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
@@ -509,10 +584,12 @@ public class HTTPClient {
/// - request: HTTP request to execute.
/// - delegate: Delegate to process response parts.
/// - deadline: Point in time by which the request must complete.
public func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
delegate: Delegate,
deadline: NIODeadline? = nil) -> Task<Delegate.Response> {
return self.execute(request: request, delegate: delegate, deadline: deadline, logger: HTTPClient.loggingDisabled)
public func execute<Delegate: HTTPClientResponseDelegate>(
request: Request,
delegate: Delegate,
deadline: NIODeadline? = nil
) -> Task<Delegate.Response> {
self.execute(request: request, delegate: delegate, deadline: deadline, logger: HTTPClient.loggingDisabled)
}
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
@@ -522,11 +599,13 @@ public class HTTPClient {
/// - delegate: Delegate to process response parts.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
delegate: Delegate,
deadline: NIODeadline? = nil,
logger: Logger) -> Task<Delegate.Response> {
return self.execute(request: request, delegate: delegate, eventLoop: .indifferent, deadline: deadline, logger: logger)
public func execute<Delegate: HTTPClientResponseDelegate>(
request: Request,
delegate: Delegate,
deadline: NIODeadline? = nil,
logger: Logger
) -> Task<Delegate.Response> {
self.execute(request: request, delegate: delegate, eventLoop: .indifferent, deadline: deadline, logger: logger)
}
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
@@ -534,18 +613,21 @@ public class HTTPClient {
/// - parameters:
/// - request: HTTP request to execute.
/// - delegate: Delegate to process response parts.
/// - eventLoop: NIO Event Loop preference.
/// - eventLoopPreference: NIO Event Loop preference.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
delegate: Delegate,
eventLoop eventLoopPreference: EventLoopPreference,
deadline: NIODeadline? = nil) -> Task<Delegate.Response> {
return self.execute(request: request,
delegate: delegate,
eventLoop: eventLoopPreference,
deadline: deadline,
logger: HTTPClient.loggingDisabled)
public func execute<Delegate: HTTPClientResponseDelegate>(
request: Request,
delegate: Delegate,
eventLoop eventLoopPreference: EventLoopPreference,
deadline: NIODeadline? = nil
) -> Task<Delegate.Response> {
self.execute(
request: request,
delegate: delegate,
eventLoop: eventLoopPreference,
deadline: deadline,
logger: HTTPClient.loggingDisabled
)
}
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
@@ -553,7 +635,7 @@ public class HTTPClient {
/// - parameters:
/// - request: HTTP request to execute.
/// - delegate: Delegate to process response parts.
/// - eventLoop: NIO Event Loop preference.
/// - eventLoopPreference: NIO Event Loop preference.
/// - deadline: Point in time by which the request must complete.
/// - logger: The logger to use for this request.
public func execute<Delegate: HTTPClientResponseDelegate>(
@@ -561,14 +643,14 @@ public class HTTPClient {
delegate: Delegate,
eventLoop eventLoopPreference: EventLoopPreference,
deadline: NIODeadline? = nil,
logger originalLogger: Logger?
logger: Logger?
) -> Task<Delegate.Response> {
self._execute(
request: request,
delegate: delegate,
eventLoop: eventLoopPreference,
deadline: deadline,
logger: originalLogger,
logger: logger,
redirectState: RedirectState(
self.configuration.redirectConfiguration.mode,
initialURL: request.url.absoluteString
@@ -592,25 +674,38 @@ public class HTTPClient {
logger originalLogger: Logger?,
redirectState: RedirectState?
) -> Task<Delegate.Response> {
let logger = (originalLogger ?? HTTPClient.loggingDisabled).attachingRequestInformation(request, requestID: globalRequestID.wrappingIncrementThenLoad(ordering: .relaxed))
let logger = (originalLogger ?? HTTPClient.loggingDisabled).attachingRequestInformation(
request,
requestID: globalRequestID.wrappingIncrementThenLoad(ordering: .relaxed)
)
let taskEL: EventLoop
switch eventLoopPreference.preference {
case .indifferent:
// if possible we want a connection on the current `EventLoop`
taskEL = self.eventLoopGroup.any()
case .delegate(on: let eventLoop):
precondition(self.eventLoopGroup.makeIterator().contains { $0 === eventLoop }, "Provided EventLoop must be part of clients EventLoopGroup.")
precondition(
self.eventLoopGroup.makeIterator().contains { $0 === eventLoop },
"Provided EventLoop must be part of clients EventLoopGroup."
)
taskEL = eventLoop
case .delegateAndChannel(on: let eventLoop):
precondition(self.eventLoopGroup.makeIterator().contains { $0 === eventLoop }, "Provided EventLoop must be part of clients EventLoopGroup.")
precondition(
self.eventLoopGroup.makeIterator().contains { $0 === eventLoop },
"Provided EventLoop must be part of clients EventLoopGroup."
)
taskEL = eventLoop
case .testOnly_exact(_, delegateOn: let delegateEL):
taskEL = delegateEL
}
logger.trace("selected EventLoop for task given the preference",
metadata: ["ahc-eventloop": "\(taskEL)",
"ahc-el-preference": "\(eventLoopPreference)"])
logger.trace(
"selected EventLoop for task given the preference",
metadata: [
"ahc-eventloop": "\(taskEL)",
"ahc-el-preference": "\(eventLoopPreference)",
]
)
let failedTask: Task<Delegate.Response>? = self.stateLock.withLock {
switch self.state {
@@ -618,10 +713,12 @@ public class HTTPClient {
return nil
case .shuttingDown, .shutDown:
logger.debug("client is shutting down, failing request")
return Task<Delegate.Response>.failedTask(eventLoop: taskEL,
error: HTTPClientError.alreadyShutdown,
logger: logger,
makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool)
return Task<Delegate.Response>.failedTask(
eventLoop: taskEL,
error: HTTPClientError.alreadyShutdown,
logger: logger,
makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool
)
}
}
@@ -644,7 +741,11 @@ public class HTTPClient {
}
}()
let task = Task<Delegate.Response>(eventLoop: taskEL, logger: logger, makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool)
let task = Task<Delegate.Response>(
eventLoop: taskEL,
logger: logger,
makeOrGetFileIOThreadPool: self.makeOrGetFileIOThreadPool
)
do {
let requestBag = try RequestBag(
request: request,
@@ -711,7 +812,12 @@ public class HTTPClient {
/// Enables automatic body decompression. Supported algorithms are gzip and deflate.
public var decompression: Decompression
/// Ignore TLS unclean shutdown error, defaults to `false`.
@available(*, deprecated, message: "AsyncHTTPClient now correctly supports handling unexpected SSL connection drops. This property is ignored")
@available(
*,
deprecated,
message:
"AsyncHTTPClient now correctly supports handling unexpected SSL connection drops. This property is ignored"
)
public var ignoreUncleanSSLShutdown: Bool {
get { false }
set {}
@@ -762,12 +868,14 @@ public class HTTPClient {
self.enableMultipath = false
}
public init(tlsConfiguration: TLSConfiguration? = nil,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled) {
public init(
tlsConfiguration: TLSConfiguration? = nil,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled
) {
self.init(
tlsConfiguration: tlsConfiguration,
redirectConfiguration: redirectConfiguration,
@@ -779,49 +887,59 @@ public class HTTPClient {
)
}
public init(certificateVerification: CertificateVerification,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
maximumAllowedIdleTimeInConnectionPool: TimeAmount = .seconds(60),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled) {
public init(
certificateVerification: CertificateVerification,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
maximumAllowedIdleTimeInConnectionPool: TimeAmount = .seconds(60),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled
) {
var tlsConfig = TLSConfiguration.makeClientConfiguration()
tlsConfig.certificateVerification = certificateVerification
self.init(tlsConfiguration: tlsConfig,
redirectConfiguration: redirectConfiguration,
timeout: timeout,
connectionPool: ConnectionPool(idleTimeout: maximumAllowedIdleTimeInConnectionPool),
proxy: proxy,
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
decompression: decompression)
self.init(
tlsConfiguration: tlsConfig,
redirectConfiguration: redirectConfiguration,
timeout: timeout,
connectionPool: ConnectionPool(idleTimeout: maximumAllowedIdleTimeInConnectionPool),
proxy: proxy,
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
decompression: decompression
)
}
public init(certificateVerification: CertificateVerification,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
connectionPool: TimeAmount = .seconds(60),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled,
backgroundActivityLogger: Logger?) {
public init(
certificateVerification: CertificateVerification,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
connectionPool: TimeAmount = .seconds(60),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled,
backgroundActivityLogger: Logger?
) {
var tlsConfig = TLSConfiguration.makeClientConfiguration()
tlsConfig.certificateVerification = certificateVerification
self.init(tlsConfiguration: tlsConfig,
redirectConfiguration: redirectConfiguration,
timeout: timeout,
connectionPool: ConnectionPool(idleTimeout: connectionPool),
proxy: proxy,
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
decompression: decompression)
self.init(
tlsConfiguration: tlsConfig,
redirectConfiguration: redirectConfiguration,
timeout: timeout,
connectionPool: ConnectionPool(idleTimeout: connectionPool),
proxy: proxy,
ignoreUncleanSSLShutdown: ignoreUncleanSSLShutdown,
decompression: decompression
)
}
public init(certificateVerification: CertificateVerification,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled) {
public init(
certificateVerification: CertificateVerification,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled
) {
self.init(
certificateVerification: certificateVerification,
redirectConfiguration: redirectConfiguration,
@@ -875,7 +993,7 @@ public class HTTPClient {
/// `EventLoop` but will not establish a new network connection just to satisfy the `EventLoop` preference if
/// another existing connection on a different `EventLoop` is readily available from a connection pool.
public static func delegate(on eventLoop: EventLoop) -> EventLoopPreference {
return EventLoopPreference(.delegate(on: eventLoop))
EventLoopPreference(.delegate(on: eventLoop))
}
/// The delegate and the `Channel` will be run on the specified EventLoop.
@@ -883,7 +1001,7 @@ public class HTTPClient {
/// Use this for use-cases where you prefer a new connection to be established over re-using an existing
/// connection that might be on a different `EventLoop`.
public static func delegateAndChannel(on eventLoop: EventLoop) -> EventLoopPreference {
return EventLoopPreference(.delegateAndChannel(on: eventLoop))
EventLoopPreference(.delegateAndChannel(on: eventLoop))
}
}
@@ -907,7 +1025,7 @@ public class HTTPClient {
extension HTTPClient.EventLoopGroupProvider {
/// Shares ``HTTPClient/defaultEventLoopGroup`` which is a singleton `EventLoopGroup` suitable for the platform.
public static var singleton: Self {
return .shared(HTTPClient.defaultEventLoopGroup)
.shared(HTTPClient.defaultEventLoopGroup)
}
}
@@ -1010,7 +1128,9 @@ extension HTTPClient.Configuration {
/// - allowCycles: Whether cycles are allowed.
///
/// - warning: Cycle detection will keep all visited URLs in memory which means a malicious server could use this as a denial-of-service vector.
public static func follow(max: Int, allowCycles: Bool) -> RedirectConfiguration { return .init(configuration: .follow(max: max, allowCycles: allowCycles)) }
public static func follow(max: Int, allowCycles: Bool) -> RedirectConfiguration {
.init(configuration: .follow(max: max, allowCycles: allowCycles))
}
}
/// Connection pool configuration.
@@ -1108,7 +1228,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
}
public var description: String {
return "HTTPClientError.\(String(describing: self.code))"
"HTTPClientError.\(String(describing: self.code))"
}
/// Short description of the error that can be used in case a bounded set of error descriptions is expected, e.g. to
@@ -1198,7 +1318,9 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
/// URL does not contain scheme.
public static let emptyScheme = HTTPClientError(code: .emptyScheme)
/// Provided URL scheme is not supported, supported schemes are: `http` and `https`
public static func unsupportedScheme(_ scheme: String) -> HTTPClientError { return HTTPClientError(code: .unsupportedScheme(scheme)) }
public static func unsupportedScheme(_ scheme: String) -> HTTPClientError {
HTTPClientError(code: .unsupportedScheme(scheme))
}
/// Request timed out while waiting for response.
public static let readTimeout = HTTPClientError(code: .readTimeout)
/// Request timed out.
@@ -1227,9 +1349,13 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
/// A body was sent in a request with method TRACE.
public static let traceRequestWithBody = HTTPClientError(code: .traceRequestWithBody)
/// Header field names contain invalid characters.
public static func invalidHeaderFieldNames(_ names: [String]) -> HTTPClientError { return HTTPClientError(code: .invalidHeaderFieldNames(names)) }
public static func invalidHeaderFieldNames(_ names: [String]) -> HTTPClientError {
HTTPClientError(code: .invalidHeaderFieldNames(names))
}
/// Header field values contain invalid characters.
public static func invalidHeaderFieldValues(_ values: [String]) -> HTTPClientError { return HTTPClientError(code: .invalidHeaderFieldValues(values)) }
public static func invalidHeaderFieldValues(_ values: [String]) -> HTTPClientError {
HTTPClientError(code: .invalidHeaderFieldValues(values))
}
/// Body length is not equal to `Content-Length`.
public static let bodyLengthMismatch = HTTPClientError(code: .bodyLengthMismatch)
/// Body part was written after request was fully sent.
@@ -1247,12 +1373,12 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
public static let tlsHandshakeTimeout = HTTPClientError(code: .tlsHandshakeTimeout)
/// The remote server only offered an unsupported application protocol
public static func serverOfferedUnsupportedApplicationProtocol(_ proto: String) -> HTTPClientError {
return HTTPClientError(code: .serverOfferedUnsupportedApplicationProtocol(proto))
HTTPClientError(code: .serverOfferedUnsupportedApplicationProtocol(proto))
}
/// The globally shared singleton ``HTTPClient`` cannot be shut down.
public static var shutdownUnsupported: HTTPClientError {
return HTTPClientError(code: .shutdownUnsupported)
HTTPClientError(code: .shutdownUnsupported)
}
/// The request deadline was exceeded. The request was cancelled because of this.
@@ -1269,6 +1395,11 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
/// - Tasks are not processed fast enough on the existing connections, to process all waiters in time
public static let getConnectionFromPoolTimeout = HTTPClientError(code: .getConnectionFromPoolTimeout)
@available(*, deprecated, message: "AsyncHTTPClient now correctly supports informational headers. For this reason `httpEndReceivedAfterHeadWith1xx` will not be thrown anymore.")
@available(
*,
deprecated,
message:
"AsyncHTTPClient now correctly supports informational headers. For this reason `httpEndReceivedAfterHeadWith1xx` will not be thrown anymore."
)
public static let httpEndReceivedAfterHeadWith1xx = HTTPClientError(code: .httpEndReceivedAfterHeadWith1xx)
}
+99 -45
View File
@@ -43,17 +43,18 @@ extension HTTPClient {
/// - parameters:
/// - data: `IOData` to write.
public func write(_ data: IOData) -> EventLoopFuture<Void> {
return self.closure(data)
self.closure(data)
}
@inlinable
func writeChunks<Bytes: Collection>(of bytes: Bytes, maxChunkSize: Int) -> EventLoopFuture<Void> where Bytes.Element == UInt8 {
func writeChunks<Bytes: Collection>(of bytes: Bytes, maxChunkSize: Int) -> EventLoopFuture<Void>
where Bytes.Element == UInt8 {
let iterator = UnsafeMutableTransferBox(bytes.chunks(ofCount: maxChunkSize).makeIterator())
guard let chunk = iterator.wrappedValue.next() else {
return self.write(IOData.byteBuffer(.init()))
}
@Sendable // can't use closure here as we recursively call ourselves which closures can't do
@Sendable // can't use closure here as we recursively call ourselves which closures can't do
func writeNextChunk(_ chunk: Bytes.SubSequence) -> EventLoopFuture<Void> {
if let nextChunk = iterator.wrappedValue.next() {
return self.write(.byteBuffer(ByteBuffer(bytes: chunk))).flatMap {
@@ -100,7 +101,7 @@ extension HTTPClient {
/// - parameters:
/// - buffer: Body `ByteBuffer` representation.
public static func byteBuffer(_ buffer: ByteBuffer) -> Body {
return Body(contentLength: Int64(buffer.readableBytes)) { writer in
Body(contentLength: Int64(buffer.readableBytes)) { writer in
writer.write(.byteBuffer(buffer))
}
}
@@ -113,8 +114,11 @@ extension HTTPClient {
/// - stream: Body chunk provider.
@_disfavoredOverload
@preconcurrency
public static func stream(length: Int? = nil, _ stream: @Sendable @escaping (StreamWriter) -> EventLoopFuture<Void>) -> Body {
return Body(contentLength: length.flatMap { Int64($0) }, stream: stream)
public static func stream(
length: Int? = nil,
_ stream: @Sendable @escaping (StreamWriter) -> EventLoopFuture<Void>
) -> Body {
Body(contentLength: length.flatMap { Int64($0) }, stream: stream)
}
/// Create and stream body using ``StreamWriter``.
@@ -122,19 +126,23 @@ extension HTTPClient {
/// - parameters:
/// - contentLength: Body size. If nil, `Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length`
/// header is set with the given `contentLength`.
/// - bodyStream: Body chunk provider.
public static func stream(contentLength: Int64? = nil, _ stream: @Sendable @escaping (StreamWriter) -> EventLoopFuture<Void>) -> Body {
return Body(contentLength: contentLength, stream: stream)
/// - stream: Body chunk provider.
public static func stream(
contentLength: Int64? = nil,
_ stream: @Sendable @escaping (StreamWriter) -> EventLoopFuture<Void>
) -> Body {
Body(contentLength: contentLength, stream: stream)
}
/// Create and stream body using a collection of bytes.
///
/// - parameters:
/// - data: Body binary representation.
/// - bytes: Body binary representation.
@preconcurrency
@inlinable
public static func bytes<Bytes>(_ bytes: Bytes) -> Body where Bytes: RandomAccessCollection, Bytes: Sendable, Bytes.Element == UInt8 {
return Body(contentLength: Int64(bytes.count)) { writer in
public static func bytes<Bytes>(_ bytes: Bytes) -> Body
where Bytes: RandomAccessCollection, Bytes: Sendable, Bytes.Element == UInt8 {
Body(contentLength: Int64(bytes.count)) { writer in
if bytes.count <= bagOfBytesToByteBufferConversionChunkSize {
return writer.write(.byteBuffer(ByteBuffer(bytes: bytes)))
} else {
@@ -148,7 +156,7 @@ extension HTTPClient {
/// - parameters:
/// - string: Body `String` representation.
public static func string(_ string: String) -> Body {
return Body(contentLength: Int64(string.utf8.count)) { writer in
Body(contentLength: Int64(string.utf8.count)) { writer in
if string.utf8.count <= bagOfBytesToByteBufferConversionChunkSize {
return writer.write(.byteBuffer(ByteBuffer(string: string)))
} else {
@@ -184,7 +192,6 @@ extension HTTPClient {
///
/// - parameters:
/// - url: Remote `URL`.
/// - version: HTTP version.
/// - method: HTTP method.
/// - headers: Custom HTTP headers.
/// - body: Request body.
@@ -193,7 +200,12 @@ extension HTTPClient {
/// - `emptyScheme` if URL does not contain HTTP scheme.
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
/// - `emptyHost` if URL does not contains a host.
public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws {
public init(
url: String,
method: HTTPMethod = .GET,
headers: HTTPHeaders = HTTPHeaders(),
body: Body? = nil
) throws {
try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil)
}
@@ -201,7 +213,6 @@ extension HTTPClient {
///
/// - parameters:
/// - url: Remote `URL`.
/// - version: HTTP version.
/// - method: HTTP method.
/// - headers: Custom HTTP headers.
/// - body: Request body.
@@ -211,7 +222,13 @@ extension HTTPClient {
/// - `emptyScheme` if URL does not contain HTTP scheme.
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
/// - `emptyHost` if URL does not contains a host.
public init(url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
public init(
url: String,
method: HTTPMethod = .GET,
headers: HTTPHeaders = HTTPHeaders(),
body: Body? = nil,
tlsConfiguration: TLSConfiguration?
) throws {
guard let url = URL(string: url) else {
throw HTTPClientError.invalidURL
}
@@ -231,7 +248,8 @@ extension HTTPClient {
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
/// - `emptyHost` if URL does not contains a host.
/// - `missingSocketPath` if URL does not contains a socketPath as an encoded host.
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws {
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws
{
try self.init(url: url, method: method, headers: headers, body: body, tlsConfiguration: nil)
}
@@ -248,7 +266,13 @@ extension HTTPClient {
/// - `unsupportedScheme` if URL does contains unsupported HTTP scheme.
/// - `emptyHost` if URL does not contains a host.
/// - `missingSocketPath` if URL does not contains a socketPath as an encoded host.
public init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil, tlsConfiguration: TLSConfiguration?) throws {
public init(
url: URL,
method: HTTPMethod = .GET,
headers: HTTPHeaders = HTTPHeaders(),
body: Body? = nil,
tlsConfiguration: TLSConfiguration?
) throws {
self.deconstructedURL = try DeconstructedURL(url: url)
self.url = url
@@ -281,7 +305,10 @@ extension HTTPClient {
head.headers.addHostIfNeeded(for: self.deconstructedURL)
let metadata = try head.headers.validateAndSetTransportFraming(method: self.method, bodyLength: .init(self.body))
let metadata = try head.headers.validateAndSetTransportFraming(
method: self.method,
bodyLength: .init(self.body)
)
return (head, metadata)
}
@@ -333,7 +360,13 @@ extension HTTPClient {
/// - version: Response HTTP version.
/// - headers: Reponse HTTP headers.
/// - body: Response body.
public init(host: String, status: HTTPResponseStatus, version: HTTPVersion, headers: HTTPHeaders, body: ByteBuffer?) {
public init(
host: String,
status: HTTPResponseStatus,
version: HTTPVersion,
headers: HTTPHeaders,
body: ByteBuffer?
) {
self.host = host
self.status = status
self.version = version
@@ -357,19 +390,19 @@ extension HTTPClient {
/// HTTP basic auth.
public static func basic(username: String, password: String) -> HTTPClient.Authorization {
return .basic(credentials: Base64.encode(bytes: "\(username):\(password)".utf8))
.basic(credentials: Base64.encode(bytes: "\(username):\(password)".utf8))
}
/// HTTP basic auth.
///
/// This version uses the raw string directly.
public static func basic(credentials: String) -> HTTPClient.Authorization {
return .init(scheme: .Basic(credentials))
.init(scheme: .Basic(credentials))
}
/// HTTP bearer auth
public static func bearer(tokens: String) -> HTTPClient.Authorization {
return .init(scheme: .Bearer(tokens))
.init(scheme: .Bearer(tokens))
}
/// The header string for this auth field.
@@ -406,7 +439,7 @@ public final class ResponseAccumulator: HTTPClientResponseDelegate {
}
public var description: String {
return "ResponseTooBigError: received response body exceeds maximum accepted size of \(self.maxBodySize) bytes"
"ResponseTooBigError: received response body exceeds maximum accepted size of \(self.maxBodySize) bytes"
}
}
@@ -450,9 +483,10 @@ public final class ResponseAccumulator: HTTPClientResponseDelegate {
switch self.state {
case .idle:
if self.requestMethod != .HEAD,
let contentLength = head.headers.first(name: "Content-Length"),
let announcedBodySize = Int(contentLength),
announcedBodySize > self.maxBodySize {
let contentLength = head.headers.first(name: "Content-Length"),
let announcedBodySize = Int(contentLength),
announcedBodySize > self.maxBodySize
{
let error = ResponseTooBigError(maxBodySize: maxBodySize)
self.state = .error(error)
return task.eventLoop.makeFailedFuture(error)
@@ -515,9 +549,21 @@ public final class ResponseAccumulator: HTTPClientResponseDelegate {
case .idle:
preconditionFailure("no head received before end")
case .head(let head):
return Response(host: self.requestHost, status: head.status, version: head.version, headers: head.headers, body: nil)
return Response(
host: self.requestHost,
status: head.status,
version: head.version,
headers: head.headers,
body: nil
)
case .body(let head, let body):
return Response(host: self.requestHost, status: head.status, version: head.version, headers: head.headers, body: body)
return Response(
host: self.requestHost,
status: head.status,
version: head.version,
headers: head.headers,
body: body
)
case .end:
preconditionFailure("request already processed")
case .error(let error):
@@ -650,14 +696,14 @@ extension HTTPClientResponseDelegate {
///
/// By default, this does nothing.
public func didReceiveHead(task: HTTPClient.Task<Response>, _: HTTPResponseHead) -> EventLoopFuture<Void> {
return task.eventLoop.makeSucceededVoidFuture()
task.eventLoop.makeSucceededVoidFuture()
}
/// Default implementation of ``HTTPClientResponseDelegate/didReceiveBodyPart(task:_:)-4fd4v``.
///
/// By default, this does nothing.
public func didReceiveBodyPart(task: HTTPClient.Task<Response>, _: ByteBuffer) -> EventLoopFuture<Void> {
return task.eventLoop.makeSucceededVoidFuture()
task.eventLoop.makeSucceededVoidFuture()
}
/// Default implementation of ``HTTPClientResponseDelegate/didReceiveError(task:_:)-fhsg``.
@@ -685,7 +731,7 @@ extension URL {
}
func hasTheSameOrigin(as other: URL) -> Bool {
return self.host == other.host && self.scheme == other.scheme && self.port == other.port
self.host == other.host && self.scheme == other.scheme && self.port == other.port
}
/// Initializes a newly created HTTP URL connecting to a unix domain socket path. The socket path is encoded as the URL's host, replacing percent encoding invalid path characters, and will use the "http+unix" scheme.
@@ -732,7 +778,7 @@ extension HTTPClient {
/// The `EventLoop` the delegate will be executed on.
public let eventLoop: EventLoop
/// The `Logger` used by the `Task` for logging.
public let logger: Logger // We are okay to store the logger here because a Task is for only one request.
public let logger: Logger // We are okay to store the logger here because a Task is for only one request.
let promise: EventLoopPromise<Response>
@@ -772,14 +818,18 @@ extension HTTPClient {
logger: Logger,
makeOrGetFileIOThreadPool: @escaping () -> NIOThreadPool
) -> Task<Response> {
let task = self.init(eventLoop: eventLoop, logger: logger, makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool)
let task = self.init(
eventLoop: eventLoop,
logger: logger,
makeOrGetFileIOThreadPool: makeOrGetFileIOThreadPool
)
task.promise.fail(error)
return task
}
/// `EventLoopFuture` for the response returned by this request.
public var futureResult: EventLoopFuture<Response> {
return self.promise.futureResult
self.promise.futureResult
}
/// Waits for execution of this request to complete.
@@ -788,7 +838,7 @@ extension HTTPClient {
/// - throws: The error value of ``futureResult`` if it errors.
@available(*, noasync, message: "wait() can block indefinitely, prefer get()", renamed: "get()")
public func wait() throws -> Response {
return try self.promise.futureResult.wait()
try self.promise.futureResult.wait()
}
/// Provides the result of this request.
@@ -797,7 +847,7 @@ extension HTTPClient {
/// - throws: The error value of ``futureResult`` if it errors.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func get() async throws -> Response {
return try await self.promise.futureResult.get()
try await self.promise.futureResult.get()
}
/// Cancels the request execution.
@@ -806,7 +856,7 @@ extension HTTPClient {
}
/// Cancels the request execution with a custom `Error`.
/// - Parameter reason: the error that is used to fail the promise
/// - Parameter error: the error that is used to fail the promise
public func fail(reason error: Error) {
let taskDelegate = self.lock.withLock { () -> HTTPClientTaskDelegate? in
self._isCancelled = true
@@ -816,15 +866,19 @@ extension HTTPClient {
taskDelegate?.fail(error)
}
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?,
with value: Response,
delegateType: Delegate.Type,
closing: Bool) {
func succeed<Delegate: HTTPClientResponseDelegate>(
promise: EventLoopPromise<Response>?,
with value: Response,
delegateType: Delegate.Type,
closing: Bool
) {
promise?.succeed(value)
}
func fail<Delegate: HTTPClientResponseDelegate>(with error: Error,
delegateType: Delegate.Type) {
func fail<Delegate: HTTPClientResponseDelegate>(
with error: Error,
delegateType: Delegate.Type
) {
self.promise.fail(error)
}
}
+5 -3
View File
@@ -52,9 +52,11 @@ struct LRUCache<Key: Equatable, Value> {
@discardableResult
mutating func append(key: Key, value: Value) -> Value {
let newElement = Element(generation: self.generation,
key: key,
value: value)
let newElement = Element(
generation: self.generation,
key: key,
value: value
)
if let found = self.bumpGenerationAndFindIndex(key: key) {
self.elements[found] = newElement
return value
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
#if canImport(Network)
import Network
#endif
import NIOCore
import NIOHTTP1
import NIOTransportServices
#if canImport(Network)
import Network
#endif
extension HTTPClient {
#if canImport(Network)
/// A wrapper for `POSIX` errors thrown by `Network.framework`.
@@ -38,7 +39,7 @@ extension HTTPClient {
self.reason = reason
}
public var description: String { return self.reason }
public var description: String { self.reason }
}
/// A wrapper for TLS errors thrown by `Network.framework`.
@@ -58,7 +59,7 @@ extension HTTPClient {
self.reason = reason
}
public var description: String { return self.reason }
public var description: String { self.reason }
}
#endif
@@ -33,7 +33,10 @@ final class NWWaitingHandler<Requester: HTTPConnectionRequester>: ChannelInbound
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
if let waitingEvent = event as? NIOTSNetworkEvents.WaitingForConnectivity {
self.requester.waitingForConnectivity(self.connectionID, error: HTTPClient.NWErrorHandler.translateError(waitingEvent.transientError))
self.requester.waitingForConnectivity(
self.connectionID,
error: HTTPClient.NWErrorHandler.translateError(waitingEvent.transientError)
)
}
context.fireUserInboundEventTriggered(event)
}
@@ -66,7 +66,10 @@ extension TLSConfiguration {
///
/// - Parameter eventLoop: EventLoop to wait for creation of options on
/// - Returns: Future holding NWProtocolTLS Options
func getNWProtocolTLSOptions(on eventLoop: EventLoop, serverNameIndicatorOverride: String?) -> EventLoopFuture<NWProtocolTLS.Options> {
func getNWProtocolTLSOptions(
on eventLoop: EventLoop,
serverNameIndicatorOverride: String?
) -> EventLoopFuture<NWProtocolTLS.Options> {
let promise = eventLoop.makePromise(of: NWProtocolTLS.Options.self)
Self.tlsDispatchQueue.async {
do {
@@ -86,11 +89,11 @@ extension TLSConfiguration {
let options = NWProtocolTLS.Options()
let useMTELGExplainer = """
You can still use this configuration option on macOS if you initialize HTTPClient \
with a MultiThreadedEventLoopGroup. Please note that using MultiThreadedEventLoopGroup \
will make AsyncHTTPClient use NIO on BSD Sockets and not Network.framework (which is the preferred \
platform networking stack).
"""
You can still use this configuration option on macOS if you initialize HTTPClient \
with a MultiThreadedEventLoopGroup. Please note that using MultiThreadedEventLoopGroup \
will make AsyncHTTPClient use NIO on BSD Sockets and not Network.framework (which is the preferred \
platform networking stack).
"""
if let serverNameIndicatorOverride = serverNameIndicatorOverride {
serverNameIndicatorOverride.withCString { serverNameIndicatorOverride in
@@ -100,15 +103,24 @@ extension TLSConfiguration {
// minimum TLS protocol
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, self.minimumTLSVersion.nwTLSProtocolVersion)
sec_protocol_options_set_min_tls_protocol_version(
options.securityProtocolOptions,
self.minimumTLSVersion.nwTLSProtocolVersion
)
} else {
sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, self.minimumTLSVersion.sslProtocol)
sec_protocol_options_set_tls_min_version(
options.securityProtocolOptions,
self.minimumTLSVersion.sslProtocol
)
}
// maximum TLS protocol
if let maximumTLSVersion = self.maximumTLSVersion {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, maximumTLSVersion.nwTLSProtocolVersion)
sec_protocol_options_set_max_tls_protocol_version(
options.securityProtocolOptions,
maximumTLSVersion.nwTLSProtocolVersion
)
} else {
sec_protocol_options_set_tls_max_version(options.securityProtocolOptions, maximumTLSVersion.sslProtocol)
}
@@ -161,8 +173,10 @@ extension TLSConfiguration {
break
}
precondition(self.certificateVerification != .noHostnameVerification,
"TLSConfiguration.certificateVerification = .noHostnameVerification is not supported. \(useMTELGExplainer)")
precondition(
self.certificateVerification != .noHostnameVerification,
"TLSConfiguration.certificateVerification = .noHostnameVerification is not supported. \(useMTELGExplainer)"
)
if certificateVerification != .fullVerification || trustRoots != nil {
// add verify block to control certificate verification
@@ -196,7 +210,8 @@ extension TLSConfiguration {
}
}
}
}, Self.tlsDispatchQueue
},
Self.tlsDispatchQueue
)
}
return options
+2 -1
View File
@@ -12,9 +12,10 @@
//
//===----------------------------------------------------------------------===//
import struct Foundation.URL
import NIOHTTP1
import struct Foundation.URL
typealias RedirectMode = HTTPClient.Configuration.RedirectConfiguration.Mode
struct RedirectState {
@@ -12,10 +12,11 @@
//
//===----------------------------------------------------------------------===//
import struct Foundation.URL
import NIOCore
import NIOHTTP1
import struct Foundation.URL
extension HTTPClient {
/// The maximum body size allowed, before a redirect response is cancelled. 3KB.
///
@@ -302,10 +303,12 @@ extension RequestBag.StateMachine {
preconditionFailure("If we receive a response, we must not have received something else before")
}
if let redirectHandler = redirectHandler, let redirectURL = redirectHandler.redirectTarget(
status: head.status,
responseHeaders: head.headers
) {
if let redirectHandler = redirectHandler,
let redirectURL = redirectHandler.redirectTarget(
status: head.status,
responseHeaders: head.headers
)
{
// If we will redirect, we need to consume the response's body ASAP, to be able to
// reuse the existing connection. We will consume a response body, if the body is
// smaller than 3kb.
@@ -348,7 +351,9 @@ extension RequestBag.StateMachine {
case .executing(let executor, let requestState, .buffering(var currentBuffer, next: let next)):
guard case .askExecutorForMore = next else {
preconditionFailure("If we have received an error or eof before, why did we get another body part? Next: \(next)")
preconditionFailure(
"If we have received an error or eof before, why did we get another body part? Next: \(next)"
)
}
self.state = .modifying
@@ -405,7 +410,9 @@ extension RequestBag.StateMachine {
case .executing(let executor, let requestState, .buffering(var buffer, next: let next)):
guard case .askExecutorForMore = next else {
preconditionFailure("If we have received an error or eof before, why did we get another body part? Next: \(next)")
preconditionFailure(
"If we have received an error or eof before, why did we get another body part? Next: \(next)"
)
}
if buffer.isEmpty, let newChunks = newChunks, !newChunks.isEmpty {
@@ -463,7 +470,9 @@ extension RequestBag.StateMachine {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("Invalid state: \(self.state)")
case .executing(_, _, .initialized):
preconditionFailure("Invalid state: Must have received response head, before this method is called for the first time")
preconditionFailure(
"Invalid state: Must have received response head, before this method is called for the first time"
)
case .executing(_, _, .buffering(_, next: .error(let connectionError))):
// if an error was received from the connection, we fail the task with the one
@@ -476,17 +485,23 @@ extension RequestBag.StateMachine {
return .failTask(error, executorToCancel: executor)
case .executing(_, _, .waitingForRemote):
preconditionFailure("Invalid state... We just returned from a consumption function. We can't already be waiting")
preconditionFailure(
"Invalid state... We just returned from a consumption function. We can't already be waiting"
)
case .redirected:
preconditionFailure("Invalid state... Redirect don't call out to delegate functions. Thus we should never land here.")
preconditionFailure(
"Invalid state... Redirect don't call out to delegate functions. Thus we should never land here."
)
case .finished(error: .some):
// don't overwrite existing errors
return .doNothing
case .finished(error: .none):
preconditionFailure("Invalid state... If no error occured, this must not be called, after the request was finished")
preconditionFailure(
"Invalid state... If no error occured, this must not be called, after the request was finished"
)
case .modifying:
preconditionFailure()
@@ -499,7 +514,9 @@ extension RequestBag.StateMachine {
preconditionFailure("Invalid state: \(self.state)")
case .executing(_, _, .initialized):
preconditionFailure("Invalid state: Must have received response head, before this method is called for the first time")
preconditionFailure(
"Invalid state: Must have received response head, before this method is called for the first time"
)
case .executing(let executor, let requestState, .buffering(var buffer, next: .askExecutorForMore)):
self.state = .modifying
@@ -529,7 +546,9 @@ extension RequestBag.StateMachine {
return .failTask(error, executorToCancel: nil)
case .executing(_, _, .waitingForRemote):
preconditionFailure("Invalid state... We just returned from a consumption function. We can't already be waiting")
preconditionFailure(
"Invalid state... We just returned from a consumption function. We can't already be waiting"
)
case .redirected:
return .doNothing
@@ -538,7 +557,9 @@ extension RequestBag.StateMachine {
return .doNothing
case .finished(error: .none):
preconditionFailure("Invalid state... If no error occurred, this must not be called, after the request was finished")
preconditionFailure(
"Invalid state... If no error occurred, this must not be called, after the request was finished"
)
case .modifying:
preconditionFailure()
@@ -559,11 +580,11 @@ extension RequestBag.StateMachine {
return .cancelScheduler(queuer)
case .initialized,
.deadlineExceededWhileQueued,
.executing,
.finished,
.redirected,
.modifying:
.deadlineExceededWhileQueued,
.executing,
.finished,
.redirected,
.modifying:
/// if we are not in the queued state, we can fail early by just calling down to `self.fail(_:)`
/// which does the appropriate state transition for us.
return .fail(self.fail(HTTPClientError.deadlineExceeded))
+11 -9
View File
@@ -58,13 +58,15 @@ final class RequestBag<Delegate: HTTPClientResponseDelegate> {
let eventLoopPreference: HTTPClient.EventLoopPreference
init(request: HTTPClient.Request,
eventLoopPreference: HTTPClient.EventLoopPreference,
task: HTTPClient.Task<Delegate.Response>,
redirectHandler: RedirectHandler<Delegate.Response>?,
connectionDeadline: NIODeadline,
requestOptions: RequestOptions,
delegate: Delegate) throws {
init(
request: HTTPClient.Request,
eventLoopPreference: HTTPClient.EventLoopPreference,
task: HTTPClient.Task<Delegate.Response>,
redirectHandler: RedirectHandler<Delegate.Response>?,
connectionDeadline: NIODeadline,
requestOptions: RequestOptions,
delegate: Delegate
) throws {
self.poolKey = .init(request, dnsOverride: requestOptions.dnsOverride)
self.eventLoopPreference = eventLoopPreference
self.task = task
@@ -435,8 +437,8 @@ extension RequestBag: HTTPExecutableRequest {
case .indifferent:
return self.task.eventLoop
case .delegate(let eventLoop),
.delegateAndChannel(on: let eventLoop),
.testOnly_exact(channelOn: let eventLoop, delegateOn: _):
.delegateAndChannel(on: let eventLoop),
.testOnly_exact(channelOn: let eventLoop, delegateOn: _):
return eventLoop
}
}
+20 -19
View File
@@ -50,23 +50,23 @@ extension HTTPHeaders {
let satisfy = name.utf8.allSatisfy { char -> Bool in
switch char {
case UInt8(ascii: "a")...UInt8(ascii: "z"),
UInt8(ascii: "A")...UInt8(ascii: "Z"),
UInt8(ascii: "0")...UInt8(ascii: "9"),
UInt8(ascii: "!"),
UInt8(ascii: "#"),
UInt8(ascii: "$"),
UInt8(ascii: "%"),
UInt8(ascii: "&"),
UInt8(ascii: "'"),
UInt8(ascii: "*"),
UInt8(ascii: "+"),
UInt8(ascii: "-"),
UInt8(ascii: "."),
UInt8(ascii: "^"),
UInt8(ascii: "_"),
UInt8(ascii: "`"),
UInt8(ascii: "|"),
UInt8(ascii: "~"):
UInt8(ascii: "A")...UInt8(ascii: "Z"),
UInt8(ascii: "0")...UInt8(ascii: "9"),
UInt8(ascii: "!"),
UInt8(ascii: "#"),
UInt8(ascii: "$"),
UInt8(ascii: "%"),
UInt8(ascii: "&"),
UInt8(ascii: "'"),
UInt8(ascii: "*"),
UInt8(ascii: "+"),
UInt8(ascii: "-"),
UInt8(ascii: "."),
UInt8(ascii: "^"),
UInt8(ascii: "_"),
UInt8(ascii: "`"),
UInt8(ascii: "|"),
UInt8(ascii: "~"):
return true
default:
return false
@@ -166,13 +166,14 @@ extension HTTPHeaders {
mutating func addHostIfNeeded(for url: DeconstructedURL) {
// if no host header was set, let's use the url host
guard !self.contains(name: "host"),
var host = url.connectionTarget.host
var host = url.connectionTarget.host
else {
return
}
// if the request uses a non-default port, we need to add it after the host
if let port = url.connectionTarget.port,
port != url.scheme.defaultPort {
port != url.scheme.defaultPort
{
host += ":\(port)"
}
self.add(name: "host", value: host)
+17 -9
View File
@@ -25,30 +25,38 @@ final class SSLContextCache {
}
extension SSLContextCache {
func sslContext(tlsConfiguration: TLSConfiguration,
eventLoop: EventLoop,
logger: Logger) -> EventLoopFuture<NIOSSLContext> {
func sslContext(
tlsConfiguration: TLSConfiguration,
eventLoop: EventLoop,
logger: Logger
) -> EventLoopFuture<NIOSSLContext> {
let eqTLSConfiguration = BestEffortHashableTLSConfiguration(wrapping: tlsConfiguration)
let sslContext = self.lock.withLock {
self.sslContextCache.find(key: eqTLSConfiguration)
}
if let sslContext = sslContext {
logger.trace("found SSL context in cache",
metadata: ["ahc-tls-config": "\(tlsConfiguration)"])
logger.trace(
"found SSL context in cache",
metadata: ["ahc-tls-config": "\(tlsConfiguration)"]
)
return eventLoop.makeSucceededFuture(sslContext)
}
logger.trace("creating new SSL context",
metadata: ["ahc-tls-config": "\(tlsConfiguration)"])
logger.trace(
"creating new SSL context",
metadata: ["ahc-tls-config": "\(tlsConfiguration)"]
)
let newSSLContext = self.offloadQueue.asyncWithFuture(eventLoop: eventLoop) {
try NIOSSLContext(configuration: tlsConfiguration)
}
newSSLContext.whenSuccess { (newSSLContext: NIOSSLContext) -> Void in
self.lock.withLock { () -> Void in
self.sslContextCache.append(key: eqTLSConfiguration,
value: newSSLContext)
self.sslContextCache.append(
key: eqTLSConfiguration,
value: newSSLContext
)
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ extension HTTPClient {
/// - `EventLoopGroup` is ``HTTPClient/defaultEventLoopGroup`` (matching the platform default)
/// - logging is disabled
public static var shared: HTTPClient {
return globallySharedHTTPClient
globallySharedHTTPClient
}
}
@@ -14,6 +14,6 @@
extension HTTPClient.EventLoopPreference: CustomStringConvertible {
public var description: String {
return "\(self.preference)"
"\(self.preference)"
}
}
+10 -5
View File
@@ -29,11 +29,11 @@ public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate {
}
public func didReceiveBodyPart(task: HTTPClient.Task<Void>, _ buffer: ByteBuffer) -> EventLoopFuture<Void> {
return self.chunkHandler(buffer)
self.chunkHandler(buffer)
}
public func didFinishRequest(task: HTTPClient.Task<Void>) throws {
return ()
()
}
}
@@ -44,7 +44,12 @@ public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate {
/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion.
@inlinable
internal func debugOnly(_ body: () -> Void) {
assert({ body(); return true }())
assert(
{
body()
return true
}()
)
}
extension BidirectionalCollection where Element: Equatable {
@@ -61,8 +66,8 @@ extension BidirectionalCollection where Element: Equatable {
guard self[ourIdx] == suffix[suffixIdx] else { return false }
}
guard suffixIdx == suffix.startIndex else {
return false // Exhausted self, but 'suffix' has elements remaining.
return false // Exhausted self, but 'suffix' has elements remaining.
}
return true // Exhausted 'other' without finding a mismatch.
return true // Exhausted 'other' without finding a mismatch.
}
}
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOPosix
import NIOSSL
import XCTest
@testable import AsyncHTTPClient
private func makeDefaultHTTPClient(
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .singleton
) -> HTTPClient {
@@ -65,9 +66,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
let request = HTTPClientRequest(url: "https://localhost:\(bin.port)/get")
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else {
return
}
@@ -85,9 +88,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
let request = HTTPClientRequest(url: "https://localhost:\(bin.port)/get")
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else {
return
}
@@ -107,13 +112,17 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.method = .POST
request.body = .bytes(ByteBuffer(string: "1234"))
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], ["4"])
guard let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -129,13 +138,17 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.method = .POST
request.body = .bytes(AnySendableSequence("1234".utf8), length: .unknown)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], [])
guard let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -151,13 +164,17 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.method = .POST
request.body = .bytes(AnySendableCollection("1234".utf8), length: .unknown)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], [])
guard let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -173,13 +190,17 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.method = .POST
request.body = .bytes(ByteBuffer(string: "1234").readableBytesView)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], ["4"])
guard let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -206,7 +227,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
}
func makeAsyncIterator() -> AsyncSequenceByteBufferGenerator {
return self
self
}
}
@@ -225,19 +246,23 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.method = .POST
let sequence = AsyncSequenceByteBufferGenerator(
chunkSize: 4_194_304, // 4MB chunk
totalChunks: 768 // Total = 3GB
chunkSize: 4_194_304, // 4MB chunk
totalChunks: 768 // Total = 3GB
)
request.body = .stream(sequence, length: .unknown)
let response: HTTPClientResponse = try await client.execute(request, deadline: .now() + .seconds(30), logger: logger)
let response: HTTPClientResponse = try await client.execute(
request,
deadline: .now() + .seconds(30),
logger: logger
)
XCTAssertEqual(response.headers["content-length"], [])
var receivedBytes: Int64 = 0
for try await part in response.body {
receivedBytes += Int64(part.readableBytes)
}
XCTAssertEqual(receivedBytes, 3_221_225_472) // 3GB
XCTAssertEqual(receivedBytes, 3_221_225_472) // 3GB
}
func testPostWithAsyncSequenceOfByteBuffers() {
@@ -249,19 +274,26 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/")
request.method = .POST
request.body = .stream([
ByteBuffer(string: "1"),
ByteBuffer(string: "2"),
ByteBuffer(string: "34"),
].async, length: .unknown)
request.body = .stream(
[
ByteBuffer(string: "1"),
ByteBuffer(string: "2"),
ByteBuffer(string: "34"),
].async,
length: .unknown
)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], [])
guard let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -277,13 +309,17 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.method = .POST
request.body = .stream("1234".utf8.async, length: .unknown)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], [])
guard let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -300,9 +336,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let streamWriter = AsyncSequenceWriter<ByteBuffer>()
request.body = .stream(streamWriter, length: .unknown)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], [])
let fragments = [
@@ -313,16 +351,20 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
var bodyIterator = response.body.makeAsyncIterator()
for expectedFragment in fragments {
streamWriter.write(expectedFragment)
guard let actualFragment = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
) else { return }
guard
let actualFragment = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
)
else { return }
XCTAssertEqual(expectedFragment, actualFragment)
}
streamWriter.end()
guard let lastResult = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
) else { return }
guard
let lastResult = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
)
else { return }
XCTAssertEqual(lastResult, nil)
}
}
@@ -339,9 +381,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let streamWriter = AsyncSequenceWriter<ByteBuffer>()
request.body = .stream(streamWriter, length: .unknown)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response.headers["content-length"], [])
let fragments = [
@@ -353,16 +397,20 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
var bodyIterator = response.body.makeAsyncIterator()
for expectedFragment in fragments {
streamWriter.write(expectedFragment)
guard let actualFragment = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
) else { return }
guard
let actualFragment = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
)
else { return }
XCTAssertEqual(expectedFragment, actualFragment)
}
streamWriter.end()
guard let lastResult = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
) else { return }
guard
let lastResult = await XCTAssertNoThrowWithResult(
try await bodyIterator.next()
)
else { return }
XCTAssertEqual(lastResult, nil)
}
}
@@ -436,7 +484,10 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
// a race between deadline and connect timer can result in either error.
// If closing happens really fast we might shutdown the pipeline before we fail the request.
// If the pipeline is closed we may receive a `.remoteConnectionClosed`.
XCTAssertTrue([.deadlineExceeded, .connectTimeout, .remoteConnectionClosed].contains(error), "unexpected error \(error)")
XCTAssertTrue(
[.deadlineExceeded, .connectTimeout, .remoteConnectionClosed].contains(error),
"unexpected error \(error)"
)
}
}
}
@@ -460,7 +511,10 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
// a race between deadline and connect timer can result in either error.
// If closing happens really fast we might shutdown the pipeline before we fail the request.
// If the pipeline is closed we may receive a `.remoteConnectionClosed`.
XCTAssertTrue([.deadlineExceeded, .connectTimeout, .remoteConnectionClosed].contains(error), "unexpected error \(error)")
XCTAssertTrue(
[.deadlineExceeded, .connectTimeout, .remoteConnectionClosed].contains(error),
"unexpected error \(error)"
)
}
}
}
@@ -500,8 +554,10 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let url = "http://localhost:\(port)/get"
#endif
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(timeout: .init(connect: .milliseconds(100), read: .milliseconds(150))))
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(timeout: .init(connect: .milliseconds(100), read: .milliseconds(150)))
)
defer {
XCTAssertNoThrow(try httpClient.syncShutdown())
@@ -549,7 +605,8 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let localClient = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
let request = HTTPClientRequest(url: "https://localhost:\(port)")
await XCTAssertThrowsError(try await localClient.execute(request, deadline: .now() + .seconds(2))) { error in
await XCTAssertThrowsError(try await localClient.execute(request, deadline: .now() + .seconds(2))) {
error in
#if canImport(Network)
guard let nwTLSError = error as? HTTPClient.NWTLSError else {
XCTFail("could not cast \(error) of type \(type(of: error)) to \(HTTPClient.NWTLSError.self)")
@@ -558,7 +615,8 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
XCTAssertEqual(nwTLSError.status, errSSLBadCert, "unexpected tls error: \(nwTLSError)")
#else
guard let sslError = error as? NIOSSLError,
case .handshakeFailed(.sslError) = sslError else {
case .handshakeFailed(.sslError) = sslError
else {
XCTFail("unexpected error \(error)")
return
}
@@ -619,7 +677,9 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let localClient = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
let request = HTTPClientRequest(url: "https://example.com:\(bin.port)/echohostheader")
let response = await XCTAssertNoThrowWithResult(try await localClient.execute(request, deadline: .now() + .seconds(2)))
let response = await XCTAssertNoThrowWithResult(
try await localClient.execute(request, deadline: .now() + .seconds(2))
)
XCTAssertEqual(response?.status, .ok)
XCTAssertEqual(response?.version, .http2)
var body = try await response?.body.collect(upTo: 1024)
@@ -634,9 +694,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let client = makeDefaultHTTPClient()
defer { XCTAssertNoThrow(try client.syncShutdown()) }
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
let request = HTTPClientRequest(url: "") // invalid URL
let request = HTTPClientRequest(url: "") // invalid URL
await XCTAssertThrowsError(try await client.execute(request, deadline: .now() + .seconds(2), logger: logger)) {
await XCTAssertThrowsError(
try await client.execute(request, deadline: .now() + .seconds(2), logger: logger)
) {
XCTAssertEqual($0 as? HTTPClientError, .invalidURL)
}
}
@@ -668,14 +730,21 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
defer { XCTAssertNoThrow(try client.syncShutdown()) }
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
var request = HTTPClientRequest(url: "https://127.0.0.1:\(bin.port)/redirect/target")
request.headers.replaceOrAdd(name: "X-Target-Redirect-URL", value: "https://localhost:\(bin.port)/echohostheader")
request.headers.replaceOrAdd(
name: "X-Target-Redirect-URL",
value: "https://localhost:\(bin.port)/echohostheader"
)
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else {
return
}
guard let body = await XCTAssertNoThrowWithResult(try await response.body.collect(upTo: 1024)) else {
return
}
guard let body = await XCTAssertNoThrowWithResult(try await response.body.collect(upTo: 1024)) else { return }
var maybeRequestInfo: RequestInfo?
XCTAssertNoThrow(maybeRequestInfo = try JSONDecoder().decode(RequestInfo.self, from: body))
guard let requestInfo = maybeRequestInfo else { return }
@@ -722,28 +791,39 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/")
request.method = .POST
request.body = .stream([
ByteBuffer(string: "1"),
ByteBuffer(string: "2"),
ByteBuffer(string: "34"),
].async, length: .unknown)
request.body = .stream(
[
ByteBuffer(string: "1"),
ByteBuffer(string: "2"),
ByteBuffer(string: "34"),
].async,
length: .unknown
)
guard let response1 = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response1 = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response1.headers["content-length"], [])
guard let body = await XCTAssertNoThrowWithResult(
try await response1.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response1.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
guard let response2 = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response2 = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
XCTAssertEqual(response2.headers["content-length"], [])
guard let body = await XCTAssertNoThrowWithResult(
try await response2.body.collect(upTo: 1024)
) else { return }
guard
let body = await XCTAssertNoThrowWithResult(
try await response2.body.collect(upTo: 1024)
)
else { return }
XCTAssertEqual(body, ByteBuffer(string: "1234"))
}
}
@@ -782,9 +862,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.headers.add(name: weirdAllowedFieldName, value: "present")
// This should work fine.
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else {
return
}
@@ -801,7 +883,9 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/get")
request.headers.add(name: forbiddenFieldName, value: "present")
await XCTAssertThrowsError(try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)) { error in
await XCTAssertThrowsError(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) { error in
XCTAssertEqual(error as? HTTPClientError, .invalidHeaderFieldNames([forbiddenFieldName]))
}
}
@@ -825,15 +909,18 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
// We reject all ASCII control characters except HTAB and tolerate everything else.
let weirdAllowedFieldValue = "!\" \t#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
let weirdAllowedFieldValue =
"!\" \t#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/get")
request.headers.add(name: "Weird-Value", value: weirdAllowedFieldValue)
// This should work fine.
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else {
return
}
@@ -850,7 +937,9 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/get")
request.headers.add(name: "Weird-Value", value: forbiddenFieldValue)
await XCTAssertThrowsError(try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)) { error in
await XCTAssertThrowsError(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) { error in
XCTAssertEqual(error as? HTTPClientError, .invalidHeaderFieldValues([forbiddenFieldValue]))
}
}
@@ -863,9 +952,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
request.headers.add(name: "Weird-Value", value: evenWeirderAllowedValue)
// This should work fine.
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else {
return
}
@@ -882,9 +973,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
defer { XCTAssertNoThrow(try client.syncShutdown()) }
let request = try HTTPClient.Request(url: "https://localhost:\(bin.port)/get")
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request: request).get()
) else {
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request: request).get()
)
else {
return
}
@@ -901,9 +994,11 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
defer { XCTAssertNoThrow(try client.syncShutdown()) }
let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
let request = HTTPClientRequest(url: "https://localhost:\(bin.port)/content-length-without-body")
guard let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
) else { return }
guard
let response = await XCTAssertNoThrowWithResult(
try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
)
else { return }
await XCTAssertThrowsError(
try await response.body.collect(upTo: 3)
) {
@@ -33,7 +33,7 @@ final class AsyncSequenceWriter<Element>: AsyncSequence, @unchecked Sendable {
}
func makeAsyncIterator() -> Iterator {
return Iterator(self)
Iterator(self)
}
private enum State {
@@ -117,7 +117,9 @@ final class AsyncSequenceWriter<Element>: AsyncSequence, @unchecked Sendable {
case .waiting:
let state = self._state
self.lock.unlock()
preconditionFailure("Expected that there is always only one concurrent call to next. Invalid state: \(state)")
preconditionFailure(
"Expected that there is always only one concurrent call to next. Invalid state: \(state)"
)
}
}
@@ -14,9 +14,6 @@
import AsyncHTTPClient
import Atomics
#if canImport(Network)
import Network
#endif
import Logging
import NIOConcurrencyHelpers
import NIOCore
@@ -29,6 +26,10 @@ import NIOTestUtils
import NIOTransportServices
import XCTest
#if canImport(Network)
import Network
#endif
final class ConnectionPoolSizeConfigValueIsRespectedTests: XCTestCaseHTTPClientTestsBaseClass {
func testConnectionPoolSizeConfigValueIsRespected() {
let numberOfRequestsPerThread = 1000
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOHTTP2
@testable import AsyncHTTPClient
extension EmbeddedChannel {
public func receiveHeadAndVerify(_ verify: (HTTPRequestHead) throws -> Void = { _ in }) throws {
let part = try self.readOutbound(as: HTTPClientRequestPart.self)
@@ -111,6 +112,6 @@ public struct HTTP1EmbeddedChannelError: Error, Hashable, CustomStringConvertibl
}
public var description: String {
return self.reason
self.reason
}
}
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOEmbedded
import NIOHTTP1
import XCTest
@testable import AsyncHTTPClient
class HTTP1ClientChannelHandlerTests: XCTestCase {
func testResponseBackpressure() {
let embedded = EmbeddedChannel()
@@ -32,27 +33,35 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
testUtils.connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(testUtils.readEventHandler.readHitCounter, 0)
embedded.read()
@@ -113,22 +122,30 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 100) { writer in
testWriter.start(writer: writer)
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 100) { writer in
testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
// the handler only writes once the channel is writable
@@ -143,12 +160,14 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
testWriter.writabilityChanged(true)
embedded.pipeline.fireChannelWritabilityChanged()
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .POST)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
XCTAssertEqual($0.headers.first(name: "content-length"), "100")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .POST)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
XCTAssertEqual($0.headers.first(name: "content-length"), "100")
}
)
// the next body write will be executed once we tick the el. before we make the channel
// unwritable
@@ -162,9 +181,11 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
embedded.embeddedEventLoop.run()
XCTAssertNoThrow(try embedded.receiveBodyAndVerify {
XCTAssertEqual($0.readableBytes, 2)
})
XCTAssertNoThrow(
try embedded.receiveBodyAndVerify {
XCTAssertEqual($0.readableBytes, 2)
}
)
XCTAssertEqual(testWriter.written, index + 1)
@@ -201,24 +222,28 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
testUtils.connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertNoThrow(try embedded.receiveEnd())
XCTAssertTrue(embedded.isActive)
@@ -247,27 +272,35 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
testUtils.connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertNoThrow(try embedded.receiveEnd())
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(testUtils.readEventHandler.readHitCounter, 0)
embedded.read()
@@ -299,27 +332,35 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
testUtils.connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertNoThrow(try embedded.receiveEnd())
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(testUtils.readEventHandler.readHitCounter, 0)
embedded.read()
@@ -345,25 +386,33 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 10) { writer in
// Advance time by more than the idle write timeout (that's 1 millisecond) to trigger the timeout.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
return testWriter.start(writer: writer)
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 10) { writer in
// Advance time by more than the idle write timeout (that's 1 millisecond) to trigger the timeout.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
return testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -383,27 +432,35 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream { _ in
// Advance time by more than the idle write timeout (that's 1 millisecond) to trigger the timeout.
let scheduled = embedded.embeddedEventLoop.flatScheduleTask(in: .milliseconds(2)) {
embedded.embeddedEventLoop.makeSucceededVoidFuture()
}
return scheduled.futureResult
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream { _ in
// Advance time by more than the idle write timeout (that's 1 millisecond) to trigger the timeout.
let scheduled = embedded.embeddedEventLoop.flatScheduleTask(in: .milliseconds(2)) {
embedded.embeddedEventLoop.makeSucceededVoidFuture()
}
return scheduled.futureResult
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(5)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(5)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -434,34 +491,42 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 10) { writer in
embedded.isWritable = false
embedded.pipeline.fireChannelWritabilityChanged()
// This should not trigger any errors or timeouts, because the timer isn't running
// as the channel is not writable.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(20))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 10) { writer in
embedded.isWritable = false
embedded.pipeline.fireChannelWritabilityChanged()
// This should not trigger any errors or timeouts, because the timer isn't running
// as the channel is not writable.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(20))
// Now that the channel will become writable, this should trigger a timeout.
embedded.isWritable = true
embedded.pipeline.fireChannelWritabilityChanged()
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
// Now that the channel will become writable, this should trigger a timeout.
embedded.isWritable = true
embedded.pipeline.fireChannelWritabilityChanged()
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
return testWriter.start(writer: writer)
}))
return testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -482,22 +547,30 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 2) { writer in
return testWriter.start(writer: writer, expectedErrors: [HTTPClientError.cancelled])
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 2) { writer in
testWriter.start(writer: writer, expectedErrors: [HTTPClientError.cancelled])
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -528,27 +601,35 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
testUtils.connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertNoThrow(try embedded.receiveEnd())
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "50")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "50")])
)
XCTAssertEqual(testUtils.readEventHandler.readHitCounter, 0)
embedded.read()
@@ -599,7 +680,12 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
XCTAssertNoThrow(maybeTestUtils = try embedded.setupHTTP1Connection())
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
XCTAssertNoThrow(try embedded.pipeline.syncOperations.addHandler(FailWriteHandler(), position: .after(testUtils.readEventHandler)))
XCTAssertNoThrow(
try embedded.pipeline.syncOperations.addHandler(
FailWriteHandler(),
position: .after(testUtils.readEventHandler)
)
)
let logger = Logger(label: "test")
@@ -609,16 +695,20 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to be able to create a request bag")
}
embedded.isWritable = false
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
@@ -645,22 +735,30 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
guard let testUtils = maybeTestUtils else { return XCTFail("Expected connection setup works") }
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 10) { writer in
testWriter.start(writer: writer)
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 10) { writer in
testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: testUtils.logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
XCTAssertNoThrow(try embedded.pipeline.addHandler(FailEndHandler(), position: .first).wait())
@@ -668,12 +766,14 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
// Execute the request and we'll receive the head.
testWriter.writabilityChanged(true)
testUtils.connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .POST)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
XCTAssertEqual($0.headers.first(name: "content-length"), "10")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .POST)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
XCTAssertEqual($0.headers.first(name: "content-length"), "10")
}
)
// We're going to immediately send the response head and end.
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertNoThrow(try embedded.writeInbound(HTTPClientResponsePart.head(responseHead)))
@@ -689,9 +789,11 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
embedded.embeddedEventLoop.run()
XCTAssertEqual(testWriter.written, 5)
for _ in 0..<5 {
XCTAssertNoThrow(try embedded.receiveBodyAndVerify {
XCTAssertEqual($0.readableBytes, 2)
})
XCTAssertNoThrow(
try embedded.receiveBodyAndVerify {
XCTAssertEqual($0.readableBytes, 2)
}
)
}
embedded.embeddedEventLoop.run()
@@ -722,10 +824,13 @@ class HTTP1ClientChannelHandlerTests: XCTestCase {
backgroundLogger: Logger(label: "no-op", factory: SwiftLogNoOpLogHandler.init),
connectionIdLoggerMetadata: "test connection"
)
let channel = EmbeddedChannel(handlers: [
ChangeWritabilityOnFlush(),
handler,
], loop: eventLoop)
let channel = EmbeddedChannel(
handlers: [
ChangeWritabilityOnFlush(),
handler,
],
loop: eventLoop
)
try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait()
let request = MockHTTPExecutableRequest()
@@ -825,7 +930,10 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
return newPromise.futureResult
case .waitingForRemote(var promiseBuffer):
assert(!promiseBuffer.isEmpty, "assert expected to be waiting if we have at least one promise in the buffer")
assert(
!promiseBuffer.isEmpty,
"assert expected to be waiting if we have at least one promise in the buffer"
)
let promise = self.eventLoop.makePromise(of: ByteBuffer?.self)
promiseBuffer.append(promise)
self.state = .waitingForRemote(promiseBuffer)
@@ -864,7 +972,10 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
func didReceiveBodyPart(task: HTTPClient.Task<Void>, _ buffer: ByteBuffer) -> EventLoopFuture<Void> {
switch self.state {
case .waitingForRemote(var promiseBuffer):
assert(!promiseBuffer.isEmpty, "assert expected to be waiting if we have at least one promise in the buffer")
assert(
!promiseBuffer.isEmpty,
"assert expected to be waiting if we have at least one promise in the buffer"
)
let promise = promiseBuffer.removeFirst()
if promiseBuffer.isEmpty {
let newBackpressurePromise = self.eventLoop.makePromise(of: Void.self)
@@ -883,7 +994,9 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
return promise.futureResult
case .buffering(.some):
preconditionFailure("Did receive response part should not be called, before the previous promise was succeeded.")
preconditionFailure(
"Did receive response part should not be called, before the previous promise was succeeded."
)
case .done, .consuming:
preconditionFailure("Invalid state: \(self.state)")
@@ -893,8 +1006,8 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
func didFinishRequest(task: HTTPClient.Task<Void>) throws {
switch self.state {
case .waitingForRemote(let promiseBuffer):
promiseBuffer.forEach {
$0.succeed(.none)
for promise in promiseBuffer {
promise.succeed(.none)
}
self.state = .done
@@ -905,7 +1018,9 @@ class ResponseBackpressureDelegate: HTTPClientResponseDelegate {
preconditionFailure("Invalid state: \(self.state)")
case .buffering(.some):
preconditionFailure("Did receive response part should not be called, before the previous promise was succeeded.")
preconditionFailure(
"Did receive response part should not be called, before the previous promise was succeeded."
)
}
}
}
@@ -12,12 +12,13 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOHTTP1
import NIOHTTPCompression
import XCTest
@testable import AsyncHTTPClient
class HTTP1ConnectionStateMachineTests: XCTestCase {
func testPOSTRequestWithWriteAndReadBackpressure() {
var state = HTTP1ConnectionStateMachine()
@@ -27,7 +28,10 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
let part1 = IOData.byteBuffer(ByteBuffer(bytes: [1]))
@@ -51,7 +55,10 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
XCTAssertEqual(state.requestStreamFinished(promise: nil), .sendRequestEnd(nil))
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.informConnectionIsIdle, .init([responseBody])))
@@ -66,10 +73,16 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["content-length": "12"])
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part0 = ByteBuffer(bytes: 0...3)
let part1 = ByteBuffer(bytes: 4...7)
let part2 = ByteBuffer(bytes: 8...11)
@@ -95,10 +108,16 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: true, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, .init([responseBody])))
@@ -112,10 +131,16 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_0, status: .ok, headers: ["content-length": "4"])
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, .init([responseBody])))
@@ -129,10 +154,20 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_0, status: .ok, headers: ["content-length": "4", "connection": "keep-alive"])
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
let responseHead = HTTPResponseHead(
version: .http1_0,
status: .ok,
headers: ["content-length": "4", "connection": "keep-alive"]
)
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.informConnectionIsIdle, .init([responseBody])))
@@ -147,10 +182,16 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["connection": "close"])
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, .init([responseBody])))
@@ -191,13 +232,19 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
let part1 = IOData.byteBuffer(ByteBuffer(bytes: [1]))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
XCTAssertEqual(state.requestStreamPartReceived(part1, promise: nil), .sendBodyPart(part1, nil))
XCTAssertEqual(state.requestCancelled(closeConnection: false), .failRequest(HTTPClientError.cancelled, .close(nil)))
XCTAssertEqual(
state.requestCancelled(closeConnection: false),
.failRequest(HTTPClientError.cancelled, .close(nil))
)
}
func testNewRequestAfterErrorHappened() {
@@ -218,9 +265,17 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
XCTAssertEqual(state.channelActive(isWritable: true), .fireChannelActive)
XCTAssertEqual(state.requestCancelled(closeConnection: false), .wait, "Should be ignored.")
XCTAssertEqual(state.requestCancelled(closeConnection: true), .close, "Should lead to connection closure.")
XCTAssertEqual(state.requestCancelled(closeConnection: true), .wait, "Should be ignored. Connection is already closing")
XCTAssertEqual(
state.requestCancelled(closeConnection: true),
.wait,
"Should be ignored. Connection is already closing"
)
XCTAssertEqual(state.channelInactive(), .fireChannelInactive)
XCTAssertEqual(state.requestCancelled(closeConnection: true), .wait, "Should be ignored. Connection is already closed")
XCTAssertEqual(
state.requestCancelled(closeConnection: true),
.wait,
"Should be ignored. Connection is already closed"
)
}
func testReadsAreForwardedIfConnectionIsClosing() {
@@ -248,7 +303,10 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["content-length": "4"])
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
XCTAssertEqual(state.runNewRequest(head: requestHead, metadata: metadata), .wait)
XCTAssertEqual(state.requestCancelled(closeConnection: false), .failRequest(HTTPClientError.cancelled, .informConnectionIsIdle))
XCTAssertEqual(
state.requestCancelled(closeConnection: false),
.failRequest(HTTPClientError.cancelled, .informConnectionIsIdle)
)
}
func testConnectionIsClosedIfErrorHappensWhileInRequest() {
@@ -258,9 +316,15 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: "Hello world!\n"))), .wait)
XCTAssertEqual(state.channelRead(.body(ByteBuffer(string: "Foo Bar!\n"))), .wait)
let decompressionError = NIOHTTPDecompression.DecompressionError.limit
@@ -274,9 +338,15 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .switchingProtocols)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, []))
}
@@ -287,8 +357,14 @@ class HTTP1ConnectionStateMachineTests: XCTestCase {
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
let newRequestAction = state.runNewRequest(head: requestHead, metadata: metadata)
XCTAssertEqual(newRequestAction, .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true))
let responseHead = HTTPResponseHead(version: .http1_1, status: .init(statusCode: 103, reasonPhrase: "Early Hints"))
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: false, startIdleTimer: true)
)
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .init(statusCode: 103, reasonPhrase: "Early Hints")
)
XCTAssertEqual(state.channelRead(.head(responseHead)), .wait)
XCTAssertEqual(state.channelInactive(), .failRequest(HTTPClientError.remoteConnectionClosed, .none))
}
@@ -339,13 +415,19 @@ extension HTTP1ConnectionStateMachine.Action: Equatable {
case (.resumeRequestBodyStream, .resumeRequestBodyStream):
return true
case (.forwardResponseHead(let lhsHead, let lhsPauseRequestBodyStream), .forwardResponseHead(let rhsHead, let rhsPauseRequestBodyStream)):
case (
.forwardResponseHead(let lhsHead, let lhsPauseRequestBodyStream),
.forwardResponseHead(let rhsHead, let rhsPauseRequestBodyStream)
):
return lhsHead == rhsHead && lhsPauseRequestBodyStream == rhsPauseRequestBodyStream
case (.forwardResponseBodyParts(let lhsData), .forwardResponseBodyParts(let rhsData)):
return lhsData == rhsData
case (.succeedRequest(let lhsFinalAction, let lhsFinalBuffer), .succeedRequest(let rhsFinalAction, let rhsFinalBuffer)):
case (
.succeedRequest(let lhsFinalAction, let lhsFinalBuffer),
.succeedRequest(let rhsFinalAction, let rhsFinalBuffer)
):
return lhsFinalAction == rhsFinalAction && lhsFinalBuffer == rhsFinalBuffer
case (.failRequest(_, let lhsFinalAction), .failRequest(_, let rhsFinalAction)):
@@ -367,7 +449,10 @@ extension HTTP1ConnectionStateMachine.Action: Equatable {
}
extension HTTP1ConnectionStateMachine.Action.FinalSuccessfulStreamAction: Equatable {
public static func == (lhs: HTTP1ConnectionStateMachine.Action.FinalSuccessfulStreamAction, rhs: HTTP1ConnectionStateMachine.Action.FinalSuccessfulStreamAction) -> Bool {
public static func == (
lhs: HTTP1ConnectionStateMachine.Action.FinalSuccessfulStreamAction,
rhs: HTTP1ConnectionStateMachine.Action.FinalSuccessfulStreamAction
) -> Bool {
switch (lhs, rhs) {
case (.close, .close):
return true
@@ -382,7 +467,10 @@ extension HTTP1ConnectionStateMachine.Action.FinalSuccessfulStreamAction: Equata
}
extension HTTP1ConnectionStateMachine.Action.FinalFailedStreamAction: Equatable {
public static func == (lhs: HTTP1ConnectionStateMachine.Action.FinalFailedStreamAction, rhs: HTTP1ConnectionStateMachine.Action.FinalFailedStreamAction) -> Bool {
public static func == (
lhs: HTTP1ConnectionStateMachine.Action.FinalFailedStreamAction,
rhs: HTTP1ConnectionStateMachine.Action.FinalFailedStreamAction
) -> Bool {
switch (lhs, rhs) {
case (.close(let lhsPromise), .close(let rhsPromise)):
return lhsPromise?.futureResult == rhsPromise?.futureResult
@@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOConcurrencyHelpers
import NIOCore
@@ -23,6 +22,8 @@ import NIOPosix
import NIOTestUtils
import XCTest
@testable import AsyncHTTPClient
class HTTP1ConnectionTests: XCTestCase {
func testCreateNewConnectionWithDecompression() {
let embedded = EmbeddedChannel()
@@ -31,16 +32,20 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertNoThrow(try embedded.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 3000)).wait())
var connection: HTTP1Connection?
XCTAssertNoThrow(connection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: MockHTTP1ConnectionDelegate(),
decompression: .enabled(limit: .ratio(4)),
logger: logger
))
XCTAssertNoThrow(
connection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: MockHTTP1ConnectionDelegate(),
decompression: .enabled(limit: .ratio(4)),
logger: logger
)
)
XCTAssertNotNil(try embedded.pipeline.syncOperations.handler(type: HTTPRequestEncoder.self))
XCTAssertNotNil(try embedded.pipeline.syncOperations.handler(type: ByteToMessageHandler<HTTPResponseDecoder>.self))
XCTAssertNotNil(
try embedded.pipeline.syncOperations.handler(type: ByteToMessageHandler<HTTPResponseDecoder>.self)
)
XCTAssertNotNil(try embedded.pipeline.syncOperations.handler(type: NIOHTTPResponseDecompressor.self))
XCTAssertNoThrow(try connection?.close().wait())
@@ -54,17 +59,22 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertNoThrow(try embedded.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 3000)).wait())
XCTAssertNoThrow(try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: MockHTTP1ConnectionDelegate(),
decompression: .disabled,
logger: logger
))
XCTAssertNoThrow(
try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: MockHTTP1ConnectionDelegate(),
decompression: .disabled,
logger: logger
)
)
XCTAssertNotNil(try embedded.pipeline.syncOperations.handler(type: HTTPRequestEncoder.self))
XCTAssertNotNil(try embedded.pipeline.syncOperations.handler(type: ByteToMessageHandler<HTTPResponseDecoder>.self))
XCTAssertThrowsError(try embedded.pipeline.syncOperations.handler(type: NIOHTTPResponseDecompressor.self)) { error in
XCTAssertNotNil(
try embedded.pipeline.syncOperations.handler(type: ByteToMessageHandler<HTTPResponseDecoder>.self)
)
XCTAssertThrowsError(try embedded.pipeline.syncOperations.handler(type: NIOHTTPResponseDecompressor.self)) {
error in
XCTAssertEqual(error as? ChannelPipelineError, .notFound)
}
}
@@ -78,13 +88,15 @@ class HTTP1ConnectionTests: XCTestCase {
embedded.embeddedEventLoop.run()
let logger = Logger(label: "test.http1.connection")
XCTAssertThrowsError(try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: MockHTTP1ConnectionDelegate(),
decompression: .disabled,
logger: logger
))
XCTAssertThrowsError(
try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: MockHTTP1ConnectionDelegate(),
decompression: .disabled,
logger: logger
)
)
}
func testGETRequest() {
@@ -113,30 +125,32 @@ class HTTP1ConnectionTests: XCTestCase {
.wait()
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(
url: "http://localhost/hello/swift",
method: .POST,
body: .stream(contentLength: 4) { writer -> EventLoopFuture<Void> in
@Sendable func recursive(count: UInt8, promise: EventLoopPromise<Void>) {
guard count < 4 else {
return promise.succeed(())
}
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/hello/swift",
method: .POST,
body: .stream(contentLength: 4) { writer -> EventLoopFuture<Void> in
@Sendable func recursive(count: UInt8, promise: EventLoopPromise<Void>) {
guard count < 4 else {
return promise.succeed(())
}
writer.write(.byteBuffer(ByteBuffer(bytes: [count]))).whenComplete { result in
switch result {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .success:
recursive(count: count + 1, promise: promise)
writer.write(.byteBuffer(ByteBuffer(bytes: [count]))).whenComplete { result in
switch result {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .success:
recursive(count: count + 1, promise: promise)
}
}
}
}
let promise = clientEL.makePromise(of: Void.self)
recursive(count: 0, promise: promise)
return promise.futureResult
}
))
let promise = clientEL.makePromise(of: Void.self)
recursive(count: 0, promise: promise)
return promise.futureResult
}
)
)
guard let request = maybeRequest else {
return XCTFail("Expected to have a connection and a request")
@@ -145,33 +159,39 @@ class HTTP1ConnectionTests: XCTestCase {
let task = HTTPClient.Task<HTTPClient.Response>(eventLoop: clientEL, logger: logger)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: clientEL),
task: task,
redirectHandler: nil,
connectionDeadline: .now() + .seconds(60),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: request)
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: clientEL),
task: task,
redirectHandler: nil,
connectionDeadline: .now() + .seconds(60),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: request)
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag.") }
connection.executeRequest(requestBag)
XCTAssertNoThrow(try server.receiveHeadAndVerify { head in
XCTAssertEqual(head.method, .POST)
XCTAssertEqual(head.uri, "/hello/swift")
XCTAssertEqual(head.headers["content-length"].first, "4")
})
XCTAssertNoThrow(
try server.receiveHeadAndVerify { head in
XCTAssertEqual(head.method, .POST)
XCTAssertEqual(head.uri, "/hello/swift")
XCTAssertEqual(head.headers["content-length"].first, "4")
}
)
var received: UInt8 = 0
while received < 4 {
XCTAssertNoThrow(try server.receiveBodyAndVerify { body in
var body = body
while let read = body.readInteger(as: UInt8.self) {
XCTAssertEqual(received, read)
received += 1
XCTAssertNoThrow(
try server.receiveBodyAndVerify { body in
var body = body
while let read = body.readInteger(as: UInt8.self) {
XCTAssertEqual(received, read)
received += 1
}
}
})
)
}
XCTAssertEqual(received, 4)
XCTAssertNoThrow(try server.receiveEnd())
@@ -198,17 +218,23 @@ class HTTP1ConnectionTests: XCTestCase {
var maybeChannel: Channel?
XCTAssertNoThrow(maybeChannel = try ClientBootstrap(group: eventLoop).connect(host: "localhost", port: httpBin.port).wait())
XCTAssertNoThrow(
maybeChannel = try ClientBootstrap(group: eventLoop).connect(host: "localhost", port: httpBin.port).wait()
)
let connectionDelegate = MockConnectionDelegate()
let logger = Logger(label: "test")
var maybeConnection: HTTP1Connection?
XCTAssertNoThrow(maybeConnection = try eventLoop.submit { try HTTP1Connection.start(
channel: XCTUnwrap(maybeChannel),
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
) }.wait())
XCTAssertNoThrow(
maybeConnection = try eventLoop.submit {
try HTTP1Connection.start(
channel: XCTUnwrap(maybeChannel),
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
)
}.wait()
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection here") }
var maybeRequest: HTTPClient.Request?
@@ -217,15 +243,17 @@ class HTTP1ConnectionTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: eventLoopGroup.next()),
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: eventLoopGroup.next()),
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
connection.executeRequest(requestBag)
@@ -248,21 +276,29 @@ class HTTP1ConnectionTests: XCTestCase {
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
let closeOnRequest = (30...100).randomElement()!
let httpBin = HTTPBin(handlerFactory: { _ in SuddenlySendsCloseHeaderChannelHandler(closeOnRequest: closeOnRequest) })
let httpBin = HTTPBin(handlerFactory: { _ in
SuddenlySendsCloseHeaderChannelHandler(closeOnRequest: closeOnRequest)
})
var maybeChannel: Channel?
XCTAssertNoThrow(maybeChannel = try ClientBootstrap(group: eventLoop).connect(host: "localhost", port: httpBin.port).wait())
XCTAssertNoThrow(
maybeChannel = try ClientBootstrap(group: eventLoop).connect(host: "localhost", port: httpBin.port).wait()
)
let connectionDelegate = MockConnectionDelegate()
let logger = Logger(label: "test")
var maybeConnection: HTTP1Connection?
XCTAssertNoThrow(maybeConnection = try eventLoop.submit { try HTTP1Connection.start(
channel: XCTUnwrap(maybeChannel),
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
) }.wait())
XCTAssertNoThrow(
maybeConnection = try eventLoop.submit {
try HTTP1Connection.start(
channel: XCTUnwrap(maybeChannel),
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
)
}.wait()
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection here") }
var counter = 0
@@ -275,16 +311,20 @@ class HTTP1ConnectionTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: eventLoopGroup.next()),
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: eventLoopGroup.next()),
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to be able to create a request bag")
}
connection.executeRequest(requestBag)
@@ -293,7 +333,7 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertEqual(response?.status, .ok)
if response?.headers.first(name: "connection") == "close" {
break // the loop
break // the loop
} else {
XCTAssertEqual(httpBin.activeConnections, 1)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, counter)
@@ -306,8 +346,11 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertEqual(counter, closeOnRequest)
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 1)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, counter - 1,
"If a close header is received connection release is not triggered.")
XCTAssertEqual(
connectionDelegate.hitConnectionReleased,
counter - 1,
"If a close header is received connection release is not triggered."
)
// we need to wait a small amount of time to see the connection close on the server
try! eventLoop.scheduleTask(in: .milliseconds(200)) {}.futureResult.wait()
@@ -324,17 +367,23 @@ class HTTP1ConnectionTests: XCTestCase {
var maybeChannel: Channel?
XCTAssertNoThrow(maybeChannel = try ClientBootstrap(group: eventLoop).connect(host: "localhost", port: httpBin.port).wait())
XCTAssertNoThrow(
maybeChannel = try ClientBootstrap(group: eventLoop).connect(host: "localhost", port: httpBin.port).wait()
)
let connectionDelegate = MockConnectionDelegate()
let logger = Logger(label: "test")
var maybeConnection: HTTP1Connection?
XCTAssertNoThrow(maybeConnection = try eventLoop.submit { try HTTP1Connection.start(
channel: XCTUnwrap(maybeChannel),
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
) }.wait())
XCTAssertNoThrow(
maybeConnection = try eventLoop.submit {
try HTTP1Connection.start(
channel: XCTUnwrap(maybeChannel),
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
)
}.wait()
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection here") }
var maybeRequest: HTTPClient.Request?
@@ -343,15 +392,17 @@ class HTTP1ConnectionTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: eventLoopGroup.next()),
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: eventLoopGroup.next()),
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
connection.executeRequest(requestBag)
@@ -373,13 +424,15 @@ class HTTP1ConnectionTests: XCTestCase {
var maybeConnection: HTTP1Connection?
let connectionDelegate = MockConnectionDelegate()
XCTAssertNoThrow(maybeConnection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
))
XCTAssertNoThrow(
maybeConnection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
)
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection at this point.") }
var maybeRequest: HTTPClient.Request?
@@ -388,38 +441,40 @@ class HTTP1ConnectionTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // head
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // end
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // head
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // end
let responseString = """
HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Accept: xAMUK7/Il9bLRFJrikq6mm8CNZI=\r\n\
Connection: upgrade\r\n\
date: Mon, 27 Sep 2021 17:53:14 GMT\r\n\
\r\n\
\r\nfoo bar baz
"""
HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Accept: xAMUK7/Il9bLRFJrikq6mm8CNZI=\r\n\
Connection: upgrade\r\n\
date: Mon, 27 Sep 2021 17:53:14 GMT\r\n\
\r\n\
\r\nfoo bar baz
"""
XCTAssertTrue(embedded.isActive)
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 0)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
XCTAssertNoThrow(try embedded.writeInbound(ByteBuffer(string: responseString)))
XCTAssertFalse(embedded.isActive)
(embedded.eventLoop as! EmbeddedEventLoop).run() // tick once to run futures.
(embedded.eventLoop as! EmbeddedEventLoop).run() // tick once to run futures.
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 1)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
@@ -438,13 +493,15 @@ class HTTP1ConnectionTests: XCTestCase {
var maybeConnection: HTTP1Connection?
let connectionDelegate = MockConnectionDelegate()
XCTAssertNoThrow(maybeConnection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
))
XCTAssertNoThrow(
maybeConnection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
)
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection at this point.") }
var maybeRequest: HTTPClient.Request?
@@ -453,28 +510,30 @@ class HTTP1ConnectionTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
connection.executeRequest(requestBag)
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // head
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // end
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // head
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // end
let responseString = """
HTTP/1.1 103 Early Hints\r\n\
date: Mon, 27 Sep 2021 17:53:14 GMT\r\n\
\r\n\
\r\n
"""
HTTP/1.1 103 Early Hints\r\n\
date: Mon, 27 Sep 2021 17:53:14 GMT\r\n\
\r\n\
\r\n
"""
XCTAssertTrue(embedded.isActive)
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 0)
@@ -484,7 +543,7 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertTrue(embedded.isActive, "The connection remains active after the informational response head")
XCTAssertNoThrow(try embedded.close().wait(), "the connection was closed")
embedded.embeddedEventLoop.run() // tick once to run futures.
embedded.embeddedEventLoop.run() // tick once to run futures.
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 1)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
@@ -500,20 +559,22 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertNoThrow(try embedded.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait())
let connectionDelegate = MockConnectionDelegate()
XCTAssertNoThrow(try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
))
XCTAssertNoThrow(
try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
)
)
let responseString = """
HTTP/1.1 200 OK\r\n\
date: Mon, 27 Sep 2021 17:53:14 GMT\r\n\
\r\n\
\r\n
"""
HTTP/1.1 200 OK\r\n\
date: Mon, 27 Sep 2021 17:53:14 GMT\r\n\
\r\n\
\r\n
"""
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 0)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
@@ -522,7 +583,7 @@ class HTTP1ConnectionTests: XCTestCase {
XCTAssertEqual($0 as? NIOHTTPDecoderError, .unsolicitedResponse)
}
XCTAssertFalse(embedded.isActive)
(embedded.eventLoop as! EmbeddedEventLoop).run() // tick once to run futures.
(embedded.eventLoop as! EmbeddedEventLoop).run() // tick once to run futures.
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 1)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
}
@@ -535,13 +596,15 @@ class HTTP1ConnectionTests: XCTestCase {
var maybeConnection: HTTP1Connection?
let connectionDelegate = MockConnectionDelegate()
XCTAssertNoThrow(maybeConnection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
))
XCTAssertNoThrow(
maybeConnection = try HTTP1Connection.start(
channel: embedded,
connectionID: 0,
delegate: connectionDelegate,
decompression: .enabled(limit: .ratio(4)),
logger: logger
)
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection at this point.") }
var maybeRequest: HTTPClient.Request?
@@ -550,32 +613,34 @@ class HTTP1ConnectionTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
connection.executeRequest(requestBag)
let responseString = """
HTTP/1.0 200 OK\r\n\
HTTP/1.0 200 OK\r\n\r\n
"""
HTTP/1.0 200 OK\r\n\
HTTP/1.0 200 OK\r\n\r\n
"""
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // head
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // end
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // head
XCTAssertNoThrow(try embedded.readOutbound(as: ByteBuffer.self)) // end
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 0)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
XCTAssertNoThrow(try embedded.writeInbound(ByteBuffer(string: responseString)))
XCTAssertFalse(embedded.isActive)
(embedded.eventLoop as! EmbeddedEventLoop).run() // tick once to run futures.
(embedded.eventLoop as! EmbeddedEventLoop).run() // tick once to run futures.
XCTAssertEqual(connectionDelegate.hitConnectionClosed, 1)
XCTAssertEqual(connectionDelegate.hitConnectionReleased, 0)
}
@@ -606,7 +671,7 @@ class HTTP1ConnectionTests: XCTestCase {
}
var reads: Int {
return self.lock.withLock {
self.lock.withLock {
self._reads
}
}
@@ -618,7 +683,7 @@ class HTTP1ConnectionTests: XCTestCase {
}
func didReceiveHead(task: HTTPClient.Task<Void>, _ head: HTTPResponseHead) -> EventLoopFuture<Void> {
return task.futureResult.eventLoop.makeSucceededVoidFuture()
task.futureResult.eventLoop.makeSucceededVoidFuture()
}
func didReceiveBodyPart(task: HTTPClient.Task<Response>, _ buffer: ByteBuffer) -> EventLoopFuture<Void> {
@@ -679,34 +744,42 @@ class HTTP1ConnectionTests: XCTestCase {
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
var maybeChannel: Channel?
XCTAssertNoThrow(maybeChannel = try ClientBootstrap(group: eventLoopGroup)
.channelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.channelOption(ChannelOptions.recvAllocator, value: FixedSizeRecvByteBufferAllocator(capacity: 1))
.connect(host: "localhost", port: httpBin.port)
.wait())
XCTAssertNoThrow(
maybeChannel = try ClientBootstrap(group: eventLoopGroup)
.channelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.channelOption(ChannelOptions.recvAllocator, value: FixedSizeRecvByteBufferAllocator(capacity: 1))
.connect(host: "localhost", port: httpBin.port)
.wait()
)
guard let channel = maybeChannel else { return XCTFail("Expected to have a channel at this point") }
let connectionDelegate = MockConnectionDelegate()
var maybeConnection: HTTP1Connection?
XCTAssertNoThrow(maybeConnection = try channel.eventLoop.submit { try HTTP1Connection.start(
channel: channel,
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
) }.wait())
XCTAssertNoThrow(
maybeConnection = try channel.eventLoop.submit {
try HTTP1Connection.start(
channel: channel,
connectionID: 0,
delegate: connectionDelegate,
decompression: .disabled,
logger: logger
)
}.wait()
)
guard let connection = maybeConnection else { return XCTFail("Expected to have a connection at this point") }
var maybeRequestBag: RequestBag<BackpressureTestDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: HTTPClient.Request(url: "http://localhost:\(httpBin.port)/custom"),
eventLoopPreference: .delegate(on: requestEventLoop),
task: .init(eventLoop: requestEventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: backpressureDelegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: HTTPClient.Request(url: "http://localhost:\(httpBin.port)/custom"),
eventLoopPreference: .delegate(on: requestEventLoop),
task: .init(eventLoop: requestEventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: backpressureDelegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
backpressureDelegate.willExecuteOnChannel(connection.channel)
@@ -764,7 +837,12 @@ class SuddenlySendsCloseHeaderChannelHandler: ChannelInboundHandler {
break
case .end:
if self.closeOnRequest == self.counter {
context.write(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok, headers: ["connection": "close"]))), promise: nil)
context.write(
self.wrapOutboundOut(
.head(.init(version: .http1_1, status: .ok, headers: ["connection": "close"]))
),
promise: nil
)
context.write(self.wrapOutboundOut(.end(nil)), promise: nil)
context.flush()
self.counter += 1
@@ -12,12 +12,13 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import NIOHTTP1
import XCTest
@testable import AsyncHTTPClient
class HTTP1ProxyConnectHandlerTests: XCTestCase {
func testProxyConnectWithoutAuthorizationSuccess() {
let embedded = EmbeddedChannel()
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOEmbedded
import NIOHTTP1
import XCTest
@testable import AsyncHTTPClient
class HTTP2ClientRequestHandlerTests: XCTestCase {
func testResponseBackpressure() {
let embedded = EmbeddedChannel()
@@ -34,28 +35,36 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.write(requestBag, promise: nil)
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertEqual(try embedded.readOutbound(as: HTTPClientRequestPart.self), .end(nil))
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(readEventHandler.readHitCounter, 0)
embedded.read()
@@ -115,22 +124,30 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let testWriter = TestBackpressureWriter(eventLoop: embedded.eventLoop, parts: 50)
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 100) { writer in
testWriter.start(writer: writer)
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 100) { writer in
testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = false
@@ -143,12 +160,14 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
testWriter.writabilityChanged(true)
embedded.pipeline.fireChannelWritabilityChanged()
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .POST)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
XCTAssertEqual($0.headers.first(name: "content-length"), "100")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .POST)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
XCTAssertEqual($0.headers.first(name: "content-length"), "100")
}
)
// the next body write will be executed once we tick the el. before we make the channel
// unwritable
@@ -162,9 +181,11 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
embedded.embeddedEventLoop.run()
XCTAssertNoThrow(try embedded.receiveBodyAndVerify {
XCTAssertEqual($0.readableBytes, 2)
})
XCTAssertNoThrow(
try embedded.receiveBodyAndVerify {
XCTAssertEqual($0.readableBytes, 2)
}
)
XCTAssertEqual(testWriter.written, index + 1)
@@ -198,27 +219,35 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.write(requestBag, promise: nil)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertNoThrow(try embedded.receiveEnd())
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(readEventHandler.readHitCounter, 0)
embedded.read()
@@ -248,27 +277,35 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.write(requestBag, promise: nil)
XCTAssertNoThrow(try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
})
XCTAssertNoThrow(
try embedded.receiveHeadAndVerify {
XCTAssertEqual($0.method, .GET)
XCTAssertEqual($0.uri, "/")
XCTAssertEqual($0.headers.first(name: "host"), "localhost")
}
)
XCTAssertNoThrow(try embedded.receiveEnd())
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(readEventHandler.readHitCounter, 0)
embedded.read()
@@ -295,24 +332,32 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let testWriter = TestBackpressureWriter(eventLoop: embedded.eventLoop, parts: 5)
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 10) { writer in
// Advance time by more than the idle write timeout (that's 1 millisecond) to trigger the timeout.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
return testWriter.start(writer: writer)
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 10) { writer in
// Advance time by more than the idle write timeout (that's 1 millisecond) to trigger the timeout.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
return testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -335,34 +380,42 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let testWriter = TestBackpressureWriter(eventLoop: embedded.eventLoop, parts: 5)
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 10) { writer in
embedded.isWritable = false
embedded.pipeline.fireChannelWritabilityChanged()
// This should not trigger any errors or timeouts, because the timer isn't running
// as the channel is not writable.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(20))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 10) { writer in
embedded.isWritable = false
embedded.pipeline.fireChannelWritabilityChanged()
// This should not trigger any errors or timeouts, because the timer isn't running
// as the channel is not writable.
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(20))
// Now that the channel will become writable, this should trigger a timeout.
embedded.isWritable = true
embedded.pipeline.fireChannelWritabilityChanged()
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
// Now that the channel will become writable, this should trigger a timeout.
embedded.isWritable = true
embedded.pipeline.fireChannelWritabilityChanged()
embedded.embeddedEventLoop.advanceTime(by: .milliseconds(2))
return testWriter.start(writer: writer)
}))
return testWriter.start(writer: writer)
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -385,22 +438,30 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let testWriter = TestBackpressureWriter(eventLoop: embedded.eventLoop, parts: 5)
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost/", method: .POST, body: .stream(contentLength: 2) { writer in
return testWriter.start(writer: writer, expectedErrors: [HTTPClientError.cancelled])
}))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost/",
method: .POST,
body: .stream(contentLength: 2) { writer in
testWriter.start(writer: writer, expectedErrors: [HTTPClientError.cancelled])
}
)
)
guard let request = maybeRequest else { return XCTFail("Expected to be able to create a request") }
let delegate = ResponseBackpressureDelegate(eventLoop: embedded.eventLoop)
var maybeRequestBag: RequestBag<ResponseBackpressureDelegate>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleWriteTimeout: .milliseconds(1)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
embedded.isWritable = true
@@ -451,16 +512,20 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let delegate = ResponseAccumulator(request: request)
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
))
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to be able to create a request bag") }
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: request,
eventLoopPreference: .delegate(on: embedded.eventLoop),
task: .init(eventLoop: embedded.eventLoop, logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(30),
requestOptions: .forTests(idleReadTimeout: .milliseconds(200)),
delegate: delegate
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to be able to create a request bag")
}
embedded.isWritable = false
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
@@ -494,10 +559,13 @@ class HTTP2ClientRequestHandlerTests: XCTestCase {
let handler = HTTP2ClientRequestHandler(
eventLoop: eventLoop
)
let channel = EmbeddedChannel(handlers: [
ChangeWritabilityOnFlush(),
handler,
], loop: eventLoop)
let channel = EmbeddedChannel(
handlers: [
ChangeWritabilityOnFlush(),
handler,
],
loop: eventLoop
)
try channel.connect(to: .init(ipAddress: "127.0.0.1", port: 80)).wait()
let request = MockHTTPExecutableRequest()
@@ -12,10 +12,7 @@
//
//===----------------------------------------------------------------------===//
/* NOT @testable */ import AsyncHTTPClient // Tests that really need @testable go into HTTP2ClientInternalTests.swift
#if canImport(Network)
import Network
#endif
import AsyncHTTPClient // NOT @testable - tests that really need @testable go into HTTP2ClientInternalTests.swift
import Logging
import NIOCore
import NIOHTTP1
@@ -23,6 +20,10 @@ import NIOPosix
import NIOSSL
import XCTest
#if canImport(Network)
import Network
#endif
class HTTP2ClientTests: XCTestCase {
func makeDefaultHTTPClient(
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .singleton
@@ -132,8 +133,8 @@ class HTTP2ClientTests: XCTestCase {
let q = DispatchQueue(label: "worker \(w)")
q.async(group: allDone) {
func go() {
allWorkersReady.signal() // tell the driver we're ready
allWorkersGo.wait() // wait for the driver to let us go
allWorkersReady.signal() // tell the driver we're ready
allWorkersGo.wait() // wait for the driver to let us go
for _ in 0..<numberOfRequestsPerWorkers {
var response: HTTPClient.Response?
@@ -195,13 +196,14 @@ class HTTP2ClientTests: XCTestCase {
let el = elg.next()
q.async(group: allDone) {
func go() {
allWorkersReady.signal() // tell the driver we're ready
allWorkersGo.wait() // wait for the driver to let us go
allWorkersReady.signal() // tell the driver we're ready
allWorkersGo.wait() // wait for the driver to let us go
for _ in 0..<numberOfRequestsPerWorkers {
var response: HTTPClient.Response?
let request = try! HTTPClient.Request(url: url)
let requestPromise = localClient
let requestPromise =
localClient
.execute(
request: request,
eventLoop: .delegateAndChannel(on: el)
@@ -251,10 +253,13 @@ class HTTP2ClientTests: XCTestCase {
XCTAssertNoThrow(try client.syncShutdown())
var results: [Result<HTTPClient.Response, Error>] = []
XCTAssertNoThrow(results = try EventLoopFuture
.whenAllComplete(responses, on: clientGroup.next())
.timeout(after: .seconds(2))
.wait())
XCTAssertNoThrow(
results =
try EventLoopFuture
.whenAllComplete(responses, on: clientGroup.next())
.timeout(after: .seconds(2))
.wait()
)
for result in results {
switch result {
@@ -397,7 +402,11 @@ class HTTP2ClientTests: XCTestCase {
XCTAssertNoThrow(maybeRequest1 = try HTTPClient.Request(url: "https://localhost:\(bin.port)/get"))
guard let request1 = maybeRequest1 else { return }
let task1 = client.execute(request: request1, delegate: ResponseAccumulator(request: request1), eventLoop: .delegateAndChannel(on: el1))
let task1 = client.execute(
request: request1,
delegate: ResponseAccumulator(request: request1),
eventLoop: .delegateAndChannel(on: el1)
)
var response1: ResponseAccumulator.Response?
XCTAssertNoThrow(response1 = try task1.wait())
@@ -408,15 +417,17 @@ class HTTP2ClientTests: XCTestCase {
let serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try serverGroup.syncShutdownGracefully()) }
var maybeServer: Channel?
XCTAssertNoThrow(maybeServer = try ServerBootstrap(group: serverGroup)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1)
.childChannelInitializer { channel in
channel.close()
}
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: "127.0.0.1", port: serverPort)
.wait())
XCTAssertNoThrow(
maybeServer = try ServerBootstrap(group: serverGroup)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1)
.childChannelInitializer { channel in
channel.close()
}
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.bind(host: "127.0.0.1", port: serverPort)
.wait()
)
// shutting down the old server closes all connections immediately
XCTAssertNoThrow(try bin.shutdown())
// client is now in HTTP/2 state and the HTTPBin is closed
@@ -427,7 +438,11 @@ class HTTP2ClientTests: XCTestCase {
XCTAssertNoThrow(maybeRequest2 = try HTTPClient.Request(url: "https://localhost:\(serverPort)/"))
guard let request2 = maybeRequest2 else { return }
let task2 = client.execute(request: request2, delegate: ResponseAccumulator(request: request2), eventLoop: .delegateAndChannel(on: el2))
let task2 = client.execute(
request: request2,
delegate: ResponseAccumulator(request: request2),
eventLoop: .delegateAndChannel(on: el2)
)
XCTAssertThrowsError(try task2.wait()) { error in
XCTAssertNil(
error as? HTTPClientError,
@@ -474,11 +489,17 @@ private final class SendHeaderAndWaitChannelHandler: ChannelInboundHandler {
let requestPart = self.unwrapInboundIn(data)
switch requestPart {
case .head:
context.writeAndFlush(self.wrapOutboundOut(.head(HTTPResponseHead(
version: HTTPVersion(major: 1, minor: 1),
status: .ok
))
), promise: nil)
context.writeAndFlush(
self.wrapOutboundOut(
.head(
HTTPResponseHead(
version: HTTPVersion(major: 1, minor: 1),
status: .ok
)
)
),
promise: nil
)
case .body, .end:
return
}
@@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOConcurrencyHelpers
import NIOCore
@@ -24,6 +23,8 @@ import NIOSSL
import NIOTestUtils
import XCTest
@testable import AsyncHTTPClient
class HTTP2ConnectionTests: XCTestCase {
func testCreateNewConnectionFailureClosedIO() {
let embedded = EmbeddedChannel()
@@ -34,14 +35,16 @@ class HTTP2ConnectionTests: XCTestCase {
embedded.embeddedEventLoop.run()
let logger = Logger(label: "test.http2.connection")
XCTAssertThrowsError(try HTTP2Connection.start(
channel: embedded,
connectionID: 0,
delegate: TestHTTP2ConnectionDelegate(),
decompression: .disabled,
maximumConnectionUses: nil,
logger: logger
).wait())
XCTAssertThrowsError(
try HTTP2Connection.start(
channel: embedded,
connectionID: 0,
delegate: TestHTTP2ConnectionDelegate(),
decompression: .disabled,
maximumConnectionUses: nil,
logger: logger
).wait()
)
}
func testConnectionToleratesShutdownEventsAfterAlreadyClosed() {
@@ -80,11 +83,12 @@ class HTTP2ConnectionTests: XCTestCase {
let connectionCreator = TestConnectionCreator()
let delegate = TestHTTP2ConnectionDelegate()
var maybeHTTP2Connection: HTTP2Connection?
XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
)
XCTAssertNoThrow(
maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
)
)
guard let http2Connection = maybeHTTP2Connection else {
return XCTFail("Expected to have an HTTP2 connection here.")
@@ -93,15 +97,17 @@ class HTTP2ConnectionTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to have a request bag at this point")
}
@@ -136,11 +142,13 @@ class HTTP2ConnectionTests: XCTestCase {
let connectionCreator = TestConnectionCreator()
let delegate = TestHTTP2ConnectionDelegate()
var maybeHTTP2Connection: HTTP2Connection?
XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
))
XCTAssertNoThrow(
maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
)
)
guard let http2Connection = maybeHTTP2Connection else {
return XCTFail("Expected to have an HTTP2 connection here.")
}
@@ -154,15 +162,17 @@ class HTTP2ConnectionTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to have a request bag at this point")
}
@@ -200,11 +210,12 @@ class HTTP2ConnectionTests: XCTestCase {
let connectionCreator = TestConnectionCreator()
let delegate = TestHTTP2ConnectionDelegate()
var maybeHTTP2Connection: HTTP2Connection?
XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
)
XCTAssertNoThrow(
maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
)
)
guard let http2Connection = maybeHTTP2Connection else {
return XCTFail("Expected to have an HTTP2 connection here.")
@@ -216,15 +227,17 @@ class HTTP2ConnectionTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to have a request bag at this point")
}
@@ -292,11 +305,13 @@ class HTTP2ConnectionTests: XCTestCase {
let connectionCreator = TestConnectionCreator()
let delegate = TestHTTP2ConnectionDelegate()
var maybeHTTP2Connection: HTTP2Connection?
XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
))
XCTAssertNoThrow(
maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
to: httpBin.port,
delegate: delegate,
on: eventLoop
)
)
guard let http2Connection = maybeHTTP2Connection else {
return XCTFail("Expected to have an HTTP2 connection here.")
}
@@ -304,15 +319,17 @@ class HTTP2ConnectionTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else {
return XCTFail("Expected to have a request bag at this point")
}
@@ -321,7 +338,9 @@ class HTTP2ConnectionTests: XCTestCase {
XCTAssertNoThrow(try serverReceivedRequestPromise.futureResult.wait())
var channelCount: Int?
XCTAssertNoThrow(channelCount = try eventLoop.submit { http2Connection.__forTesting_getStreamChannels().count }.wait())
XCTAssertNoThrow(
channelCount = try eventLoop.submit { http2Connection.__forTesting_getStreamChannels().count }.wait()
)
XCTAssertEqual(channelCount, 1)
triggerResponsePromise.succeed(())
@@ -331,7 +350,9 @@ class HTTP2ConnectionTests: XCTestCase {
var retryCount = 0
let maxRetries = 1000
while retryCount < maxRetries {
XCTAssertNoThrow(channelCount = try eventLoop.submit { http2Connection.__forTesting_getStreamChannels().count }.wait())
XCTAssertNoThrow(
channelCount = try eventLoop.submit { http2Connection.__forTesting_getStreamChannels().count }.wait()
)
if channelCount == 0 {
break
}
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOEmbedded
import NIOHTTP2
import XCTest
@testable import AsyncHTTPClient
class HTTP2IdleHandlerTests: XCTestCase {
func testReceiveSettingsWithMaxConcurrentStreamSetting() {
let delegate = MockHTTP2IdleHandlerDelegate()
@@ -26,7 +27,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
@@ -41,7 +45,11 @@ class HTTP2IdleHandlerTests: XCTestCase {
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([])))
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 100, "Expected to assume 100 maxConcurrentConnection, if no setting was present")
XCTAssertEqual(
delegate.maxStreams,
100,
"Expected to assume 100 maxConcurrentConnection, if no setting was present"
)
}
func testEmptySettingsDontOverwriteMaxConcurrentStreamSetting() {
@@ -50,7 +58,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
@@ -66,12 +77,18 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
let emptySettings = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 20)])))
let emptySettings = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 20)]))
)
XCTAssertNoThrow(try embedded.writeInbound(emptySettings))
XCTAssertEqual(delegate.maxStreams, 20)
}
@@ -83,7 +100,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let randomStreamID = HTTP2StreamID((0..<Int32.max).randomElement()!)
let goAwayFrame = HTTP2Frame(streamID: randomStreamID, payload: .goAway(lastStreamID: 0, errorCode: .http11Required, opaqueData: nil))
let goAwayFrame = HTTP2Frame(
streamID: randomStreamID,
payload: .goAway(lastStreamID: 0, errorCode: .http11Required, opaqueData: nil)
)
XCTAssertEqual(delegate.goAwayReceived, false)
XCTAssertNoThrow(try embedded.writeInbound(goAwayFrame))
XCTAssertEqual(delegate.goAwayReceived, true)
@@ -100,13 +120,19 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
let randomStreamID = HTTP2StreamID((0..<Int32.max).randomElement()!)
let goAwayFrame = HTTP2Frame(streamID: randomStreamID, payload: .goAway(lastStreamID: 0, errorCode: .http11Required, opaqueData: nil))
let goAwayFrame = HTTP2Frame(
streamID: randomStreamID,
payload: .goAway(lastStreamID: 0, errorCode: .http11Required, opaqueData: nil)
)
XCTAssertEqual(delegate.goAwayReceived, false)
XCTAssertNoThrow(try embedded.writeInbound(goAwayFrame))
XCTAssertEqual(delegate.goAwayReceived, true)
@@ -138,7 +164,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
@@ -154,7 +183,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
@@ -163,7 +195,11 @@ class HTTP2IdleHandlerTests: XCTestCase {
for i in 0..<(1...100).randomElement()! {
let streamID = HTTP2StreamID(i)
let event = NIOHTTP2StreamCreatedEvent(streamID: streamID, localInitialWindowSize: nil, remoteInitialWindowSize: nil)
let event = NIOHTTP2StreamCreatedEvent(
streamID: streamID,
localInitialWindowSize: nil,
remoteInitialWindowSize: nil
)
embedded.pipeline.fireUserInboundEventTriggered(event)
openStreams.insert(streamID)
}
@@ -191,7 +227,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
@@ -200,13 +239,20 @@ class HTTP2IdleHandlerTests: XCTestCase {
for i in 0..<(1...100).randomElement()! {
let streamID = HTTP2StreamID(i)
let event = NIOHTTP2StreamCreatedEvent(streamID: streamID, localInitialWindowSize: nil, remoteInitialWindowSize: nil)
let event = NIOHTTP2StreamCreatedEvent(
streamID: streamID,
localInitialWindowSize: nil,
remoteInitialWindowSize: nil
)
embedded.pipeline.fireUserInboundEventTriggered(event)
openStreams.insert(streamID)
}
let goAwayStreamID = HTTP2StreamID(openStreams.count)
let goAwayFrame = HTTP2Frame(streamID: goAwayStreamID, payload: .goAway(lastStreamID: 0, errorCode: .http11Required, opaqueData: nil))
let goAwayFrame = HTTP2Frame(
streamID: goAwayStreamID,
payload: .goAway(lastStreamID: 0, errorCode: .http11Required, opaqueData: nil)
)
XCTAssertEqual(delegate.goAwayReceived, false)
XCTAssertNoThrow(try embedded.writeInbound(goAwayFrame))
XCTAssertEqual(delegate.goAwayReceived, true)
@@ -232,7 +278,10 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 10)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 10)
@@ -241,12 +290,18 @@ class HTTP2IdleHandlerTests: XCTestCase {
embedded.pipeline.triggerUserOutboundEvent(HTTPConnectionEvent.shutdownRequested, promise: nil)
XCTAssertFalse(embedded.isActive)
let newSettingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 20)])))
let newSettingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 20)]))
)
XCTAssertEqual(delegate.maxStreams, 10)
XCTAssertNoThrow(try embedded.writeInbound(newSettingsFrame))
XCTAssertEqual(delegate.maxStreams, 10, "Expected message to not be forwarded.")
let goAwayFrame = HTTP2Frame(streamID: HTTP2StreamID(0), payload: .goAway(lastStreamID: 2, errorCode: .http11Required, opaqueData: nil))
let goAwayFrame = HTTP2Frame(
streamID: HTTP2StreamID(0),
payload: .goAway(lastStreamID: 2, errorCode: .http11Required, opaqueData: nil)
)
XCTAssertEqual(delegate.goAwayReceived, false)
XCTAssertNoThrow(try embedded.writeInbound(goAwayFrame))
XCTAssertEqual(delegate.goAwayReceived, false, "Expected go away to not be forwarded.")
@@ -258,19 +313,30 @@ class HTTP2IdleHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [idleHandler])
XCTAssertNoThrow(try embedded.connect(to: .makeAddressResolvingHost("localhost", port: 0)).wait())
let settingsFrame = HTTP2Frame(streamID: 0, payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 100)])))
let settingsFrame = HTTP2Frame(
streamID: 0,
payload: .settings(.settings([.init(parameter: .maxConcurrentStreams, value: 100)]))
)
XCTAssertEqual(delegate.maxStreams, nil)
XCTAssertNoThrow(try embedded.writeInbound(settingsFrame))
XCTAssertEqual(delegate.maxStreams, 100)
for streamID in HTTP2StreamID(1)..<HTTP2StreamID(5) {
let event = NIOHTTP2StreamCreatedEvent(streamID: streamID, localInitialWindowSize: nil, remoteInitialWindowSize: nil)
let event = NIOHTTP2StreamCreatedEvent(
streamID: streamID,
localInitialWindowSize: nil,
remoteInitialWindowSize: nil
)
embedded.pipeline.fireUserInboundEventTriggered(event)
XCTAssertFalse(delegate.goAwayReceived)
}
// Open one the last stream.
let event = NIOHTTP2StreamCreatedEvent(streamID: HTTP2StreamID(5), localInitialWindowSize: nil, remoteInitialWindowSize: nil)
let event = NIOHTTP2StreamCreatedEvent(
streamID: HTTP2StreamID(5),
localInitialWindowSize: nil,
remoteInitialWindowSize: nil
)
embedded.pipeline.fireUserInboundEventTriggered(event)
XCTAssertTrue(delegate.goAwayReceived)
@@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//
/* NOT @testable */ import AsyncHTTPClient // Tests that need @testable go into HTTPClientInternalTests.swift
import AsyncHTTPClient // NOT @testable - tests that need @testable go into HTTPClientInternalTests.swift
import Logging
import NIOCore
import NIOPosix
@@ -29,7 +29,7 @@ class HTTPClientSOCKSTests: XCTestCase {
var backgroundLogStore: CollectEverythingLogHandler.LogStore!
var defaultHTTPBinURLPrefix: String {
return "http://localhost:\(self.defaultHTTPBin.port)/"
"http://localhost:\(self.defaultHTTPBin.port)/"
}
override func setUp() {
@@ -43,12 +43,17 @@ class HTTPClientSOCKSTests: XCTestCase {
self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.defaultHTTPBin = HTTPBin()
self.backgroundLogStore = CollectEverythingLogHandler.LogStore()
var backgroundLogger = Logger(label: "\(#function)", factory: { _ in
CollectEverythingLogHandler(logStore: self.backgroundLogStore!)
})
var backgroundLogger = Logger(
label: "\(#function)",
factory: { _ in
CollectEverythingLogHandler(logStore: self.backgroundLogStore!)
}
)
backgroundLogger.logLevel = .trace
self.defaultClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
backgroundActivityLogger: backgroundLogger)
self.defaultClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
backgroundActivityLogger: backgroundLogger
)
}
override func tearDown() {
@@ -75,10 +80,12 @@ class HTTPClientSOCKSTests: XCTestCase {
func testProxySOCKS() throws {
let socksBin = try MockSOCKSServer(expectedURL: "/socks/test", expectedResponse: "it works!")
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(
proxy: .socksServer(host: "localhost", port: socksBin.port)
).enableFastFailureModeForTesting())
let localClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(
proxy: .socksServer(host: "localhost", port: socksBin.port)
).enableFastFailureModeForTesting()
)
defer {
XCTAssertNoThrow(try localClient.syncShutdown())
@@ -94,8 +101,10 @@ class HTTPClientSOCKSTests: XCTestCase {
func testProxySOCKSBogusAddress() throws {
let config = HTTPClient.Configuration(proxy: .socksServer(host: "127.0.."))
.enableFastFailureModeForTesting()
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config)
let localClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config
)
defer {
XCTAssertNoThrow(try localClient.syncShutdown())
@@ -109,8 +118,10 @@ class HTTPClientSOCKSTests: XCTestCase {
let config = HTTPClient.Configuration(proxy: .socksServer(host: "localhost", port: localHTTPBin.port))
.enableFastFailureModeForTesting()
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config)
let localClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config
)
defer {
XCTAssertNoThrow(try localClient.syncShutdown())
XCTAssertNoThrow(try localHTTPBin.shutdown())
@@ -123,8 +134,10 @@ class HTTPClientSOCKSTests: XCTestCase {
let config = HTTPClient.Configuration(proxy: .socksServer(host: "localhost"))
.enableFastFailureModeForTesting()
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config)
let localClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config
)
defer {
XCTAssertNoThrow(try localClient.syncShutdown())
}
@@ -137,8 +150,10 @@ class HTTPClientSOCKSTests: XCTestCase {
let config = HTTPClient.Configuration(proxy: .socksServer(host: "localhost", port: socksBin.port))
.enableFastFailureModeForTesting()
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config)
let localClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config
)
defer {
XCTAssertNoThrow(try localClient.syncShutdown())
@@ -14,9 +14,6 @@
import AsyncHTTPClient
import Atomics
#if canImport(Network)
import Network
#endif
import Logging
import NIOConcurrencyHelpers
import NIOCore
@@ -29,6 +26,10 @@ import NIOTestUtils
import NIOTransportServices
import XCTest
#if canImport(Network)
import Network
#endif
class XCTestCaseHTTPClientTestsBaseClass: XCTestCase {
typealias Request = HTTPClient.Request
@@ -53,13 +54,18 @@ class XCTestCaseHTTPClientTestsBaseClass: XCTestCase {
self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.defaultHTTPBin = HTTPBin()
self.backgroundLogStore = CollectEverythingLogHandler.LogStore()
var backgroundLogger = Logger(label: "\(#function)", factory: { _ in
CollectEverythingLogHandler(logStore: self.backgroundLogStore!)
})
var backgroundLogger = Logger(
label: "\(#function)",
factory: { _ in
CollectEverythingLogHandler(logStore: self.backgroundLogStore!)
}
)
backgroundLogger.logLevel = .trace
self.defaultClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: HTTPClient.Configuration().enableFastFailureModeForTesting(),
backgroundActivityLogger: backgroundLogger)
self.defaultClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: HTTPClient.Configuration().enableFastFailureModeForTesting(),
backgroundActivityLogger: backgroundLogger
)
}
override func tearDown() {
@@ -19,7 +19,8 @@ import XCTest
class HTTPClientCookieTests: XCTestCase {
func testCookie() {
let v = "key=value; PaTh=/path; DoMaIn=EXampLE.CoM; eXpIRes=Wed, 21 Oct 2015 07:28:00 GMT; max-AGE=42; seCURE; HTTPOnly"
let v =
"key=value; PaTh=/path; DoMaIn=EXampLE.CoM; eXpIRes=Wed, 21 Oct 2015 07:28:00 GMT; max-AGE=42; seCURE; HTTPOnly"
guard let c = HTTPClient.Cookie(header: v, defaultDomain: "exAMPle.cOm") else {
XCTFail("Failed to parse cookie")
return
@@ -67,7 +68,16 @@ class HTTPClientCookieTests: XCTestCase {
}
func testCookieInit() {
let c = HTTPClient.Cookie(name: "key", value: "value", path: "/path", domain: "example.com", expires: Date(timeIntervalSince1970: 1_445_412_480), maxAge: 42, httpOnly: true, secure: true)
let c = HTTPClient.Cookie(
name: "key",
value: "value",
path: "/path",
domain: "example.com",
expires: Date(timeIntervalSince1970: 1_445_412_480),
maxAge: 42,
httpOnly: true,
secure: true
)
XCTAssertEqual("key", c.name)
XCTAssertEqual("value", c.value)
XCTAssertEqual("/path", c.path)
@@ -118,17 +128,26 @@ class HTTPClientCookieTests: XCTestCase {
XCTAssertNil(c?.expires)
// Later values override earlier values, except if they are ignored.
c = HTTPClient.Cookie(header: "key=value; expires=Sunday, 06-Nov-94 08:49:37 GMT; expires=04/01/2022", defaultDomain: "example.com")
c = HTTPClient.Cookie(
header: "key=value; expires=Sunday, 06-Nov-94 08:49:37 GMT; expires=04/01/2022",
defaultDomain: "example.com"
)
XCTAssertEqual("key", c?.name)
XCTAssertEqual("value", c?.value)
XCTAssertEqual(Date(timeIntervalSince1970: 784_111_777), c?.expires)
c = HTTPClient.Cookie(header: "key=value; expires=Sunday, 06-Nov-94 08:49:37 GMT; expires=", defaultDomain: "example.com")
c = HTTPClient.Cookie(
header: "key=value; expires=Sunday, 06-Nov-94 08:49:37 GMT; expires=",
defaultDomain: "example.com"
)
XCTAssertEqual("key", c?.name)
XCTAssertEqual("value", c?.value)
XCTAssertEqual(Date(timeIntervalSince1970: 784_111_777), c?.expires)
c = HTTPClient.Cookie(header: "key=value; expires=Sunday, 06-Nov-94 08:49:37 GMT; expires", defaultDomain: "example.com")
c = HTTPClient.Cookie(
header: "key=value; expires=Sunday, 06-Nov-94 08:49:37 GMT; expires",
defaultDomain: "example.com"
)
XCTAssertEqual("key", c?.name)
XCTAssertEqual("value", c?.value)
XCTAssertEqual(Date(timeIntervalSince1970: 784_111_777), c?.expires)
@@ -467,11 +486,17 @@ class HTTPClientCookieTests: XCTestCase {
try XCTSkipIf(localeCheck.tm_mon != 1, "Unable to set locale")
// Cookie parsing should be independent of C locale.
var c = HTTPClient.Cookie(header: "key=value; eXpIRes=Sunday, 06-Nov-94 08:49:37 GMT;", defaultDomain: "example.org")
var c = HTTPClient.Cookie(
header: "key=value; eXpIRes=Sunday, 06-Nov-94 08:49:37 GMT;",
defaultDomain: "example.org"
)
XCTAssertEqual(Date(timeIntervalSince1970: 784_111_777), c?.expires)
c = HTTPClient.Cookie(header: "key=value; eXpIRes=Sun Nov 6 08:49:37 1994;", defaultDomain: "example.org")!
XCTAssertEqual(Date(timeIntervalSince1970: 784_111_777), c?.expires)
c = HTTPClient.Cookie(header: "key=value; eXpIRes=Sonntag, 06-Nov-94 08:49:37 GMT;", defaultDomain: "example.org")!
c = HTTPClient.Cookie(
header: "key=value; eXpIRes=Sonntag, 06-Nov-94 08:49:37 GMT;",
defaultDomain: "example.org"
)!
XCTAssertNil(c?.expires)
}
}
@@ -27,7 +27,10 @@ final class HTTPClientReproTests: XCTestCase {
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch self.unwrapInboundIn(data) {
case .head:
context.writeAndFlush(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .continue))), promise: nil)
context.writeAndFlush(
self.wrapOutboundOut(.head(.init(version: .http1_1, status: .continue))),
promise: nil
)
case .body:
break
case .end:
@@ -47,14 +50,16 @@ final class HTTPClientReproTests: XCTestCase {
let body = #"{"foo": "bar"}"#
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(
url: "http://localhost:\(httpBin.port)/",
method: .POST,
headers: [
"Content-Type": "application/json",
],
body: .string(body)
))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost:\(httpBin.port)/",
method: .POST,
headers: [
"Content-Type": "application/json"
],
body: .string(body)
)
)
guard let request = maybeRequest else { return XCTFail("Expected to have a request here") }
var logger = Logger(label: "test")
@@ -73,10 +78,14 @@ final class HTTPClientReproTests: XCTestCase {
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch self.unwrapInboundIn(data) {
case .head:
let head = HTTPResponseHead(version: .http1_1, status: .switchingProtocols, headers: [
"Connection": "Upgrade",
"Upgrade": "Websocket",
])
let head = HTTPResponseHead(
version: .http1_1,
status: .switchingProtocols,
headers: [
"Connection": "Upgrade",
"Upgrade": "Websocket",
]
)
let body = context.channel.allocator.buffer(string: "foo bar")
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
@@ -101,14 +110,16 @@ final class HTTPClientReproTests: XCTestCase {
let body = #"{"foo": "bar"}"#
var maybeRequest: HTTPClient.Request?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(
url: "http://localhost:\(httpBin.port)/",
method: .POST,
headers: [
"Content-Type": "application/json",
],
body: .string(body)
))
XCTAssertNoThrow(
maybeRequest = try HTTPClient.Request(
url: "http://localhost:\(httpBin.port)/",
method: .POST,
headers: [
"Content-Type": "application/json"
],
body: .string(body)
)
)
guard let request = maybeRequest else { return XCTFail("Expected to have a request here") }
var logger = Logger(label: "test")
@@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOConcurrencyHelpers
import NIOCore
import NIOEmbedded
@@ -21,6 +20,8 @@ import NIOPosix
import NIOTestUtils
import XCTest
@testable import AsyncHTTPClient
class HTTPClientInternalTests: XCTestCase {
typealias Request = HTTPClient.Request
typealias Task = HTTPClient.Task
@@ -204,15 +205,19 @@ class HTTPClientInternalTests: XCTestCase {
self.receivedMessages.append(.error(error))
}
public func didReceiveHead(task: HTTPClient.Task<Response>,
_ head: HTTPResponseHead) -> EventLoopFuture<Void> {
public func didReceiveHead(
task: HTTPClient.Task<Response>,
_ head: HTTPResponseHead
) -> EventLoopFuture<Void> {
self.eventLoop.assertInEventLoop()
self.receivedMessages.append(.head(head))
return self.randoEL.makeSucceededFuture(())
}
func didReceiveBodyPart(task: HTTPClient.Task<Response>,
_ buffer: ByteBuffer) -> EventLoopFuture<Void> {
func didReceiveBodyPart(
task: HTTPClient.Task<Response>,
_ buffer: ByteBuffer
) -> EventLoopFuture<Void> {
self.eventLoop.assertInEventLoop()
self.receivedMessages.append(.bodyPart(buffer))
return self.randoEL.makeSucceededFuture(())
@@ -250,22 +255,38 @@ class HTTPClientInternalTests: XCTestCase {
}
}
let request = try Request(url: "http://127.0.0.1:\(server.serverPort)/custom",
body: body)
let request = try Request(
url: "http://127.0.0.1:\(server.serverPort)/custom",
body: body
)
let delegate = Delegate(expectedEventLoop: delegateEL, randomOtherEventLoop: randoEL)
let future = httpClient.execute(request: request,
delegate: delegate,
eventLoop: .init(.testOnly_exact(channelOn: channelEL,
delegateOn: delegateEL))).futureResult
let future = httpClient.execute(
request: request,
delegate: delegate,
eventLoop: .init(
.testOnly_exact(
channelOn: channelEL,
delegateOn: delegateEL
)
)
).futureResult
XCTAssertNoThrow(try server.readInbound()) // .head
XCTAssertNoThrow(try server.readInbound()) // .body
XCTAssertNoThrow(try server.readInbound()) // .end
XCTAssertNoThrow(try server.readInbound()) // .head
XCTAssertNoThrow(try server.readInbound()) // .body
XCTAssertNoThrow(try server.readInbound()) // .end
// Send 3 parts, but only one should be received until the future is complete
XCTAssertNoThrow(try server.writeOutbound(.head(.init(version: .init(major: 1, minor: 1),
status: .ok,
headers: HTTPHeaders([("Transfer-Encoding", "chunked")])))))
XCTAssertNoThrow(
try server.writeOutbound(
.head(
.init(
version: .init(major: 1, minor: 1),
status: .ok,
headers: HTTPHeaders([("Transfer-Encoding", "chunked")])
)
)
)
)
let buffer = ByteBuffer(string: "1234")
XCTAssertNoThrow(try server.writeOutbound(.body(.byteBuffer(buffer))))
XCTAssertNoThrow(try server.writeOutbound(.end(nil)))
@@ -297,7 +318,7 @@ class HTTPClientInternalTests: XCTestCase {
switch sentMessages.dropFirst(3).first {
case .some(.sentRequest):
() // OK
() // OK
default:
XCTFail("wrong message")
}
@@ -335,7 +356,10 @@ class HTTPClientInternalTests: XCTestCase {
let el = group.next()
let req1 = client.execute(request: request, eventLoop: .delegate(on: el))
let req2 = client.execute(request: request, eventLoop: .delegateAndChannel(on: el))
let req3 = client.execute(request: request, eventLoop: .init(.testOnly_exact(channelOn: el, delegateOn: el)))
let req3 = client.execute(
request: request,
eventLoop: .init(.testOnly_exact(channelOn: el, delegateOn: el))
)
XCTAssert(req1.eventLoop === el)
XCTAssert(req2.eventLoop === el)
XCTAssert(req3.eventLoop === el)
@@ -354,8 +378,8 @@ class HTTPClientInternalTests: XCTestCase {
_ = httpClient.get(url: "http://localhost:\(server.serverPort)/wait")
XCTAssertNoThrow(try server.readInbound()) // .head
XCTAssertNoThrow(try server.readInbound()) // .end
XCTAssertNoThrow(try server.readInbound()) // .head
XCTAssertNoThrow(try server.readInbound()) // .end
do {
try httpClient.syncShutdown(requiresCleanClose: true)
@@ -395,10 +419,16 @@ class HTTPClientInternalTests: XCTestCase {
}
}
let request = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/post", method: .POST, body: body)
let response = httpClient.execute(request: request,
delegate: ResponseAccumulator(request: request),
eventLoop: HTTPClient.EventLoopPreference(.testOnly_exact(channelOn: el2,
delegateOn: el1)))
let response = httpClient.execute(
request: request,
delegate: ResponseAccumulator(request: request),
eventLoop: HTTPClient.EventLoopPreference(
.testOnly_exact(
channelOn: el2,
delegateOn: el1
)
)
)
XCTAssert(el1 === response.eventLoop)
XCTAssertNoThrow(try response.wait())
}
@@ -419,7 +449,11 @@ class HTTPClientInternalTests: XCTestCase {
let request = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)//get")
let delegate = ResponseAccumulator(request: request)
let task = client.execute(request: request, delegate: delegate, eventLoop: .init(.testOnly_exact(channelOn: el1, delegateOn: el2)))
let task = client.execute(
request: request,
delegate: delegate,
eventLoop: .init(.testOnly_exact(channelOn: el1, delegateOn: el2))
)
XCTAssertTrue(task.futureResult.eventLoop === el2)
XCTAssertNoThrow(try task.wait())
}
@@ -460,7 +494,11 @@ class HTTPClientInternalTests: XCTestCase {
let request = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/get")
let delegate = TestDelegate(expectedEL: el1)
XCTAssertNoThrow(try httpBin.shutdown())
let task = client.execute(request: request, delegate: delegate, eventLoop: .init(.testOnly_exact(channelOn: el2, delegateOn: el1)))
let task = client.execute(
request: request,
delegate: delegate,
eventLoop: .init(.testOnly_exact(channelOn: el2, delegateOn: el1))
)
XCTAssertThrowsError(try task.wait())
XCTAssertTrue(delegate.receivedError)
}
@@ -493,10 +531,13 @@ class HTTPClientInternalTests: XCTestCase {
let request6 = try Request(url: "https://127.0.0.1")
XCTAssertEqual(request6.deconstructedURL.scheme, .https)
XCTAssertEqual(request6.deconstructedURL.connectionTarget, .ipAddress(
serialization: "127.0.0.1",
address: try! SocketAddress(ipAddress: "127.0.0.1", port: 443)
))
XCTAssertEqual(
request6.deconstructedURL.connectionTarget,
.ipAddress(
serialization: "127.0.0.1",
address: try! SocketAddress(ipAddress: "127.0.0.1", port: 443)
)
)
XCTAssertEqual(request6.deconstructedURL.uri, "/")
let request7 = try Request(url: "https://0x7F.1:9999")
@@ -506,18 +547,24 @@ class HTTPClientInternalTests: XCTestCase {
let request8 = try Request(url: "http://[::1]")
XCTAssertEqual(request8.deconstructedURL.scheme, .http)
XCTAssertEqual(request8.deconstructedURL.connectionTarget, .ipAddress(
serialization: "[::1]",
address: try! SocketAddress(ipAddress: "::1", port: 80)
))
XCTAssertEqual(
request8.deconstructedURL.connectionTarget,
.ipAddress(
serialization: "[::1]",
address: try! SocketAddress(ipAddress: "::1", port: 80)
)
)
XCTAssertEqual(request8.deconstructedURL.uri, "/")
let request9 = try Request(url: "http://[763e:61d9::6ACA:3100:6274]:4242/foo/bar?baz")
XCTAssertEqual(request9.deconstructedURL.scheme, .http)
XCTAssertEqual(request9.deconstructedURL.connectionTarget, .ipAddress(
serialization: "[763e:61d9::6ACA:3100:6274]",
address: try! SocketAddress(ipAddress: "763e:61d9::6aca:3100:6274", port: 4242)
))
XCTAssertEqual(
request9.deconstructedURL.connectionTarget,
.ipAddress(
serialization: "[763e:61d9::6ACA:3100:6274]",
address: try! SocketAddress(ipAddress: "763e:61d9::6aca:3100:6274", port: 4242)
)
)
XCTAssertEqual(request9.deconstructedURL.uri, "/foo/bar?baz")
// Some systems have quirks in their implementations of 'ntop' which cause them to write
@@ -526,18 +573,24 @@ class HTTPClientInternalTests: XCTestCase {
// so the serialization must be kept verbatim as it was given in the request.
let request10 = try Request(url: "http://[::c0a8:1]:4242/foo/bar?baz")
XCTAssertEqual(request10.deconstructedURL.scheme, .http)
XCTAssertEqual(request10.deconstructedURL.connectionTarget, .ipAddress(
serialization: "[::c0a8:1]",
address: try! SocketAddress(ipAddress: "::c0a8:1", port: 4242)
))
XCTAssertEqual(
request10.deconstructedURL.connectionTarget,
.ipAddress(
serialization: "[::c0a8:1]",
address: try! SocketAddress(ipAddress: "::c0a8:1", port: 4242)
)
)
XCTAssertEqual(request10.deconstructedURL.uri, "/foo/bar?baz")
let request11 = try Request(url: "http://[::192.168.0.1]:4242/foo/bar?baz")
XCTAssertEqual(request11.deconstructedURL.scheme, .http)
XCTAssertEqual(request11.deconstructedURL.connectionTarget, .ipAddress(
serialization: "[::192.168.0.1]",
address: try! SocketAddress(ipAddress: "::192.168.0.1", port: 4242)
))
XCTAssertEqual(
request11.deconstructedURL.connectionTarget,
.ipAddress(
serialization: "[::192.168.0.1]",
address: try! SocketAddress(ipAddress: "::192.168.0.1", port: 4242)
)
)
XCTAssertEqual(request11.deconstructedURL.uri, "/foo/bar?baz")
}
@@ -566,7 +619,7 @@ class HTTPClientInternalTests: XCTestCase {
}
// Empty collection.
do {
let elements: Array<Int> = []
let elements: [Int] = []
XCTAssertTrue(elements.hasSuffix([]))
XCTAssertFalse(elements.hasSuffix([0]))
XCTAssertFalse(elements.hasSuffix([42]))
@@ -12,10 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
#if canImport(Network)
import Network
#endif
import NIOConcurrencyHelpers
import NIOCore
import NIOPosix
@@ -23,6 +19,12 @@ import NIOSSL
import NIOTransportServices
import XCTest
@testable import AsyncHTTPClient
#if canImport(Network)
import Network
#endif
class HTTPClientNIOTSTests: XCTestCase {
var clientGroup: EventLoopGroup!
@@ -57,8 +59,10 @@ class HTTPClientNIOTSTests: XCTestCase {
let httpBin = HTTPBin(.http1_1(ssl: true))
let config = HTTPClient.Configuration()
.enableFastFailureModeForTesting()
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config)
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config
)
defer {
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
XCTAssertNoThrow(try httpBin.shutdown())
@@ -69,8 +73,10 @@ class HTTPClientNIOTSTests: XCTestCase {
_ = try httpClient.get(url: "https://localhost:\(httpBin.port)/get").wait()
XCTFail("This should have failed")
} catch let error as HTTPClient.NWTLSError {
XCTAssert(error.status == errSSLHandshakeFail || error.status == errSSLBadCert,
"unexpected NWTLSError with status \(error.status)")
XCTAssert(
error.status == errSSLHandshakeFail || error.status == errSSLBadCert,
"unexpected NWTLSError with status \(error.status)"
)
} catch {
XCTFail("Error should have been NWTLSError not \(type(of: error))")
}
@@ -86,8 +92,10 @@ class HTTPClientNIOTSTests: XCTestCase {
let config = HTTPClient.Configuration()
.enableFastFailureModeForTesting()
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config)
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: config
)
defer {
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
@@ -106,9 +114,15 @@ class HTTPClientNIOTSTests: XCTestCase {
guard isTestingNIOTS() else { return }
#if canImport(Network)
let httpBin = HTTPBin(.http1_1(ssl: false))
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(timeout: .init(connect: .milliseconds(100),
read: .milliseconds(100))))
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(
timeout: .init(
connect: .milliseconds(100),
read: .milliseconds(100)
)
)
)
defer {
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
@@ -13,10 +13,11 @@
//===----------------------------------------------------------------------===//
import Algorithms
@testable import AsyncHTTPClient
import NIOCore
import XCTest
@testable import AsyncHTTPClient
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
class HTTPClientRequestTests: XCTestCase {
private typealias Request = HTTPClientRequest
@@ -27,31 +28,40 @@ class HTTPClientRequestTests: XCTestCase {
XCTAsyncTest {
var request = Request(url: "https://example.com/get")
request.headers = [
"custom-header": "custom-header-value",
"custom-header": "custom-header-value"
]
var preparedRequest: PreparedRequest?
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .https,
connectionTarget: .domain(name: "example.com", port: 443),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .GET,
uri: "/get",
headers: [
"host": "example.com",
"custom-header": "custom-header-value",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .https,
connectionTarget: .domain(name: "example.com", port: 443),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .GET,
uri: "/get",
headers: [
"host": "example.com",
"custom-header": "custom-header-value",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
}
@@ -76,22 +86,31 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .unix,
connectionTarget: .unixSocket(path: "/some_path"),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .GET,
uri: "/",
headers: ["custom-header": "custom-value"]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .unix,
connectionTarget: .unixSocket(path: "/some_path"),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .GET,
uri: "/",
headers: ["custom-header": "custom-value"]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
}
@@ -105,22 +124,31 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .httpUnix,
connectionTarget: .unixSocket(path: "/example/folder.sock"),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .GET,
uri: "/some_path",
headers: ["custom-header": "custom-value"]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .httpUnix,
connectionTarget: .unixSocket(path: "/example/folder.sock"),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .GET,
uri: "/some_path",
headers: ["custom-header": "custom-value"]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
}
@@ -134,22 +162,31 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .httpsUnix,
connectionTarget: .unixSocket(path: "/example/folder.sock"),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .GET,
uri: "/some_path",
headers: ["custom-header": "custom-value"]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .httpsUnix,
connectionTarget: .unixSocket(path: "/example/folder.sock"),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .GET,
uri: "/some_path",
headers: ["custom-header": "custom-value"]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
}
@@ -162,22 +199,31 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .https,
connectionTarget: .domain(name: "example.com", port: 443),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .GET,
uri: "/get",
headers: ["host": "example.com"]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .https,
connectionTarget: .domain(name: "example.com", port: 443),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .GET,
uri: "/get",
headers: ["host": "example.com"]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
}
@@ -191,25 +237,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "0",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "0",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
@@ -225,25 +280,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "0",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(0)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "0",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(0)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, ByteBuffer())
@@ -259,25 +323,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(9)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(9)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, .init(string: "post body"))
}
@@ -293,25 +366,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"transfer-encoding": "chunked",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .stream
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"transfer-encoding": "chunked",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .stream
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, .init(string: "post body"))
}
@@ -328,25 +410,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(9)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(9)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, .init(string: "post body"))
}
@@ -362,25 +453,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(9)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(9)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, .init(string: "post body"))
}
@@ -401,25 +501,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"transfer-encoding": "chunked",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .stream
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"transfer-encoding": "chunked",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .stream
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, .init(string: "post body"))
}
@@ -440,25 +549,34 @@ class HTTPClientRequestTests: XCTestCase {
XCTAssertNoThrow(preparedRequest = try PreparedRequest(request))
guard let preparedRequest = preparedRequest else { return }
XCTAssertEqual(preparedRequest.poolKey, .init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
))
XCTAssertEqual(preparedRequest.head, .init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
))
XCTAssertEqual(preparedRequest.requestFramingMetadata, .init(
connectionClose: false,
body: .fixedSize(9)
))
XCTAssertEqual(
preparedRequest.poolKey,
.init(
scheme: .http,
connectionTarget: .domain(name: "example.com", port: 80),
tlsConfiguration: nil,
serverNameIndicatorOverride: nil
)
)
XCTAssertEqual(
preparedRequest.head,
.init(
version: .http1_1,
method: .POST,
uri: "/post",
headers: [
"host": "example.com",
"content-length": "9",
]
)
)
XCTAssertEqual(
preparedRequest.requestFramingMetadata,
.init(
connectionClose: false,
body: .fixedSize(9)
)
)
guard let buffer = await XCTAssertNoThrowWithResult(try await preparedRequest.body.read()) else { return }
XCTAssertEqual(buffer, .init(string: "post body"))
}
@@ -466,9 +584,9 @@ class HTTPClientRequestTests: XCTestCase {
func testChunkingRandomAccessCollection() async throws {
let body = try await HTTPClientRequest.Body.bytes(
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
).collect()
let expectedChunks = [
@@ -482,11 +600,9 @@ class HTTPClientRequestTests: XCTestCase {
func testChunkingCollection() async throws {
let body = try await HTTPClientRequest.Body.bytes(
(
String(repeating: "0", count: bagOfBytesToByteBufferConversionChunkSize) +
String(repeating: "1", count: bagOfBytesToByteBufferConversionChunkSize) +
String(repeating: "2", count: bagOfBytesToByteBufferConversionChunkSize)
).utf8,
(String(repeating: "0", count: bagOfBytesToByteBufferConversionChunkSize)
+ String(repeating: "1", count: bagOfBytesToByteBufferConversionChunkSize)
+ String(repeating: "2", count: bagOfBytesToByteBufferConversionChunkSize)).utf8,
length: .known(Int64(bagOfBytesToByteBufferConversionChunkSize * 3))
).collect()
@@ -503,8 +619,8 @@ class HTTPClientRequestTests: XCTestCase {
let bagOfBytesToByteBufferConversionChunkSize = 8
let body = try await HTTPClientRequest.Body._bytes(
AnySequence(
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize)
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize)
),
length: .known(Int64(bagOfBytesToByteBufferConversionChunkSize * 3)),
bagOfBytesToByteBufferConversionChunkSize: bagOfBytesToByteBufferConversionChunkSize,
@@ -521,9 +637,9 @@ class HTTPClientRequestTests: XCTestCase {
func testChunkingSequenceFastPath() async throws {
func makeBytes() -> some Sequence<UInt8> & Sendable {
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
}
let body = try await HTTPClientRequest.Body.bytes(
makeBytes(),
@@ -534,7 +650,7 @@ class HTTPClientRequestTests: XCTestCase {
firstChunk.writeImmutableBuffer(ByteBuffer(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize))
firstChunk.writeImmutableBuffer(ByteBuffer(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize))
let expectedChunks = [
firstChunk,
firstChunk
]
XCTAssertEqual(body, expectedChunks)
@@ -544,9 +660,9 @@ class HTTPClientRequestTests: XCTestCase {
let bagOfBytesToByteBufferConversionChunkSize = 8
let byteBufferMaxSize = 16
func makeBytes() -> some Sequence<UInt8> & Sendable {
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
}
let body = try await HTTPClientRequest.Body._bytes(
makeBytes(),
@@ -568,9 +684,9 @@ class HTTPClientRequestTests: XCTestCase {
func testBodyStringChunking() throws {
let body = try HTTPClient.Body.string(
String(repeating: "0", count: bagOfBytesToByteBufferConversionChunkSize) +
String(repeating: "1", count: bagOfBytesToByteBufferConversionChunkSize) +
String(repeating: "2", count: bagOfBytesToByteBufferConversionChunkSize)
String(repeating: "0", count: bagOfBytesToByteBufferConversionChunkSize)
+ String(repeating: "1", count: bagOfBytesToByteBufferConversionChunkSize)
+ String(repeating: "2", count: bagOfBytesToByteBufferConversionChunkSize)
).collect().wait()
let expectedChunks = [
@@ -584,9 +700,9 @@ class HTTPClientRequestTests: XCTestCase {
func testBodyChunkingRandomAccessCollection() throws {
let body = try HTTPClient.Body.bytes(
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize) +
Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
Array(repeating: 0, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 1, count: bagOfBytesToByteBufferConversionChunkSize)
+ Array(repeating: 2, count: bagOfBytesToByteBufferConversionChunkSize)
).collect().wait()
let expectedChunks = [
@@ -642,7 +758,8 @@ extension Optional where Wrapped == HTTPClientRequest.Prepared.Body {
case .sequence(let announcedLength, _, let generate):
let buffer = generate(ByteBufferAllocator())
if case .known(let announcedLength) = announcedLength,
announcedLength != Int64(buffer.readableBytes) {
announcedLength != Int64(buffer.readableBytes)
{
throw LengthMismatch(announcedLength: announcedLength, actualLength: Int64(buffer.readableBytes))
}
return buffer
@@ -652,8 +769,12 @@ extension Optional where Wrapped == HTTPClientRequest.Prepared.Body {
accumulatedBuffer.writeBuffer(&buffer)
}
if case .known(let announcedLength) = announcedLength,
announcedLength != Int64(accumulatedBuffer.readableBytes) {
throw LengthMismatch(announcedLength: announcedLength, actualLength: Int64(accumulatedBuffer.readableBytes))
announcedLength != Int64(accumulatedBuffer.readableBytes)
{
throw LengthMismatch(
announcedLength: announcedLength,
actualLength: Int64(accumulatedBuffer.readableBytes)
)
}
return accumulatedBuffer
}
@@ -12,25 +12,38 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import XCTest
@testable import AsyncHTTPClient
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
final class HTTPClientResponseTests: XCTestCase {
func testSimpleResponse() {
let response = HTTPClientResponse.expectedContentLength(requestMethod: .GET, headers: ["content-length": "1025"], status: .ok)
let response = HTTPClientResponse.expectedContentLength(
requestMethod: .GET,
headers: ["content-length": "1025"],
status: .ok
)
XCTAssertEqual(response, 1025)
}
func testSimpleResponseNotModified() {
let response = HTTPClientResponse.expectedContentLength(requestMethod: .GET, headers: ["content-length": "1025"], status: .notModified)
let response = HTTPClientResponse.expectedContentLength(
requestMethod: .GET,
headers: ["content-length": "1025"],
status: .notModified
)
XCTAssertEqual(response, 0)
}
func testSimpleResponseHeadRequestMethod() {
let response = HTTPClientResponse.expectedContentLength(requestMethod: .HEAD, headers: ["content-length": "1025"], status: .ok)
let response = HTTPClientResponse.expectedContentLength(
requestMethod: .HEAD,
headers: ["content-length": "1025"],
status: .ok
)
XCTAssertEqual(response, 0)
}
@@ -40,7 +53,11 @@ final class HTTPClientResponseTests: XCTestCase {
}
func testResponseInvalidInteger() {
let response = HTTPClientResponse.expectedContentLength(requestMethod: .GET, headers: ["content-length": "none"], status: .ok)
let response = HTTPClientResponse.expectedContentLength(
requestMethod: .GET,
headers: ["content-length": "none"],
status: .ok
)
XCTAssertEqual(response, nil)
}
}
@@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Atomics
import Foundation
import Logging
@@ -28,6 +27,9 @@ import NIOSSL
import NIOTLS
import NIOTransportServices
import XCTest
@testable import AsyncHTTPClient
#if canImport(xlocale)
import xlocale
#elseif canImport(locale_h)
@@ -52,7 +54,8 @@ func isTestingNIOTS() -> Bool {
func getDefaultEventLoopGroup(numberOfThreads: Int) -> EventLoopGroup {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *),
isTestingNIOTS() {
isTestingNIOTS()
{
return NIOTSEventLoopGroup(loopCount: numberOfThreads, defaultQoS: .default)
}
#endif
@@ -144,7 +147,7 @@ class CountingDelegate: HTTPClientResponseDelegate {
}
func didFinishRequest(task: HTTPClient.Task<Response>) throws -> Int {
return self.count
self.count
}
}
@@ -219,8 +222,8 @@ enum TemporaryFileHelpers {
} else {
return "/tmp"
}
#endif // os
#endif // targetEnvironment
#endif // os
#endif // targetEnvironment
}
private static func openTemporaryFile() -> (CInt, String) {
@@ -240,8 +243,10 @@ enum TemporaryFileHelpers {
///
/// If the temporary directory is too long to store a UNIX domain socket path, it will `chdir` into the temporary
/// directory and return a short-enough path. The iOS simulator is known to have too long paths.
internal static func withTemporaryUnixDomainSocketPathName<T>(directory: String = temporaryDirectory,
_ body: (String) throws -> T) throws -> T {
internal static func withTemporaryUnixDomainSocketPathName<T>(
directory: String = temporaryDirectory,
_ body: (String) throws -> T
) throws -> T {
// this is racy but we're trying to create the shortest possible path so we can't add a directory...
let (fd, path) = self.openTemporaryFile()
close(fd)
@@ -256,10 +261,14 @@ enum TemporaryFileHelpers {
shortEnoughPath = path
restoreSavedCWD = false
} catch SocketAddressError.unixDomainSocketPathTooLong {
FileManager.default.changeCurrentDirectoryPath(URL(fileURLWithPath: path).deletingLastPathComponent().absoluteString)
FileManager.default.changeCurrentDirectoryPath(
URL(fileURLWithPath: path).deletingLastPathComponent().absoluteString
)
shortEnoughPath = URL(fileURLWithPath: path).lastPathComponent
restoreSavedCWD = true
print("WARNING: Path '\(path)' could not be used as UNIX domain socket path, using chdir & '\(shortEnoughPath)'")
print(
"WARNING: Path '\(path)' could not be used as UNIX domain socket path, using chdir & '\(shortEnoughPath)'"
)
}
defer {
if FileManager.default.fileExists(atPath: path) {
@@ -307,11 +316,11 @@ enum TemporaryFileHelpers {
}
internal static func fileSize(path: String) throws -> Int? {
return try FileManager.default.attributesOfItem(atPath: path)[.size] as? Int
try FileManager.default.attributesOfItem(atPath: path)[.size] as? Int
}
internal static func fileExists(path: String) -> Bool {
return FileManager.default.fileExists(atPath: path)
FileManager.default.fileExists(atPath: path)
}
}
@@ -324,9 +333,11 @@ enum TestTLS {
)
}
internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
internal final class HTTPBin<RequestHandler: ChannelInboundHandler>
where
RequestHandler.InboundIn == HTTPServerRequestPart,
RequestHandler.OutboundOut == HTTPServerResponsePart {
RequestHandler.OutboundOut == HTTPServerResponsePart
{
enum BindTarget {
case unixDomainSocket(String)
case localhostIPv4RandomPort
@@ -393,19 +404,19 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
private let activeConnCounterHandler: ConnectionsCountHandler
var activeConnections: Int {
return self.activeConnCounterHandler.currentlyActiveConnections
self.activeConnCounterHandler.currentlyActiveConnections
}
var createdConnections: Int {
return self.activeConnCounterHandler.createdConnections
self.activeConnCounterHandler.createdConnections
}
var port: Int {
return Int(self.serverChannel.localAddress!.port!)
Int(self.serverChannel.localAddress!.port!)
}
var socketAddress: SocketAddress {
return self.serverChannel.localAddress!
self.serverChannel.localAddress!
}
var baseURL: String {
@@ -464,7 +475,10 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
self.serverChannel = try! ServerBootstrap(group: self.group)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: reusePort ? 1 : 0)
.serverChannelOption(
ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT),
value: reusePort ? 1 : 0
)
.serverChannelInitializer { channel in
channel.pipeline.addHandler(self.activeConnCounterHandler)
}.childChannelInitializer { channel in
@@ -673,7 +687,11 @@ final class HTTPProxySimulator: ChannelInboundHandler, RemovableChannelHandler {
init(promise: EventLoopPromise<Void>, expectedAuthorization: String?) {
self.promise = promise
self.expectedAuthorization = expectedAuthorization
self.head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: .init([("Content-Length", "0")]))
self.head = HTTPResponseHead(
version: .init(major: 1, minor: 1),
status: .ok,
headers: .init([("Content-Length", "0")])
)
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
@@ -687,7 +705,8 @@ final class HTTPProxySimulator: ChannelInboundHandler, RemovableChannelHandler {
if let expectedAuthorization = self.expectedAuthorization {
guard let authorization = head.headers["proxy-authorization"].first,
expectedAuthorization == authorization else {
expectedAuthorization == authorization
else {
self.head.status = .proxyAuthenticationRequired
return
}
@@ -712,7 +731,11 @@ internal struct HTTPResponseBuilder {
var head: HTTPResponseHead
var body: ByteBuffer?
init(_ version: HTTPVersion = HTTPVersion(major: 1, minor: 1), status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders()) {
init(
_ version: HTTPVersion = HTTPVersion(major: 1, minor: 1),
status: HTTPResponseStatus,
headers: HTTPHeaders = HTTPHeaders()
) {
self.head = HTTPResponseHead(version: version, status: status, headers: headers)
}
@@ -764,8 +787,10 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
for header in head.headers {
let needle = "x-send-back-header-"
if header.name.lowercased().starts(with: needle) {
self.responseHeaders.add(name: String(header.name.dropFirst(needle.count)),
value: header.value)
self.responseHeaders.add(
name: String(header.name.dropFirst(needle.count)),
value: header.value
)
}
}
}
@@ -778,7 +803,12 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
headers = HTTPHeaders()
}
context.write(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))), promise: nil)
context.write(
wrapOutboundOut(
.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))
),
promise: nil
)
for i in 0..<10 {
let msg = "id: \(i)"
var buf = context.channel.allocator.buffer(capacity: msg.count)
@@ -793,7 +823,12 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
// This tests receiving chunks very fast: please do not insert delays here!
let headers = HTTPHeaders([("Transfer-Encoding", "chunked")])
context.write(self.wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))), promise: nil)
context.write(
self.wrapOutboundOut(
.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))
),
promise: nil
)
for i in 0..<10 {
let msg = "id: \(i)"
var buf = context.channel.allocator.buffer(capacity: msg.count)
@@ -808,7 +843,12 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
// This tests receiving a lot of tiny chunks: they must all be sent in a single flush or the test doesn't work.
let headers = HTTPHeaders([("Transfer-Encoding", "chunked")])
context.write(self.wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))), promise: nil)
context.write(
self.wrapOutboundOut(
.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))
),
promise: nil
)
let message = ByteBuffer(integer: UInt8(ascii: "a"))
// This number (10k) is load-bearing and a bit magic: it has been experimentally verified as being sufficient to blow the stack
@@ -928,9 +968,12 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
context.close(promise: nil)
return
case "/custom":
context.writeAndFlush(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok))), promise: nil)
context.writeAndFlush(
wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok))),
promise: nil
)
return
case "/events/10/1": // TODO: parse path
case "/events/10/1": // TODO: parse path
self.writeEvents(context: context)
return
case "/events/10/content-length":
@@ -954,10 +997,20 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
case "/content-length-without-body":
var headers = self.responseHeaders
headers.replaceOrAdd(name: "content-length", value: "1234")
context.writeAndFlush(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))), promise: nil)
context.writeAndFlush(
wrapOutboundOut(
.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .ok, headers: headers))
),
promise: nil
)
return
default:
context.write(wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .notFound))), promise: nil)
context.write(
wrapOutboundOut(
.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: .notFound))
),
promise: nil
)
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
return
}
@@ -976,18 +1029,26 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
response.head.headers.add(contentsOf: self.responseHeaders)
context.write(wrapOutboundOut(.head(response.head)), promise: nil)
if let body = response.body {
let requestInfo = RequestInfo(data: String(buffer: body),
requestNumber: self.requestId,
connectionNumber: self.connectionID)
let responseBody = try! JSONEncoder().encodeAsByteBuffer(requestInfo,
allocator: context.channel.allocator)
let requestInfo = RequestInfo(
data: String(buffer: body),
requestNumber: self.requestId,
connectionNumber: self.connectionID
)
let responseBody = try! JSONEncoder().encodeAsByteBuffer(
requestInfo,
allocator: context.channel.allocator
)
context.write(wrapOutboundOut(.body(.byteBuffer(responseBody))), promise: nil)
} else {
let requestInfo = RequestInfo(data: "",
requestNumber: self.requestId,
connectionNumber: self.connectionID)
let responseBody = try! JSONEncoder().encodeAsByteBuffer(requestInfo,
allocator: context.channel.allocator)
let requestInfo = RequestInfo(
data: "",
requestNumber: self.requestId,
connectionNumber: self.connectionID
)
let responseBody = try! JSONEncoder().encodeAsByteBuffer(
requestInfo,
allocator: context.channel.allocator
)
context.write(wrapOutboundOut(.body(.byteBuffer(responseBody))), promise: nil)
}
context.eventLoop.scheduleTask(in: self.delay) {
@@ -1000,8 +1061,9 @@ internal final class HTTPBinHandler: ChannelInboundHandler {
self.isServingRequest = false
switch result {
case .success:
if self.responseHeaders[canonicalForm: "X-Close-Connection"].contains("true") ||
self.shouldClose {
if self.responseHeaders[canonicalForm: "X-Close-Connection"].contains("true")
|| self.shouldClose
{
context.close(promise: nil)
}
case .failure(let error):
@@ -1170,7 +1232,7 @@ struct CollectEverythingLogHandler: LogHandler {
var allEntries: [Entry] {
get {
return self.lock.withLock { self.logs }
self.lock.withLock { self.logs }
}
set {
self.lock.withLock { self.logs = newValue }
@@ -1179,9 +1241,13 @@ struct CollectEverythingLogHandler: LogHandler {
func append(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?) {
self.lock.withLock {
self.logs.append(Entry(level: level,
message: message.description,
metadata: metadata?.mapValues { $0.description } ?? [:]))
self.logs.append(
Entry(
level: level,
message: message.description,
metadata: metadata?.mapValues { $0.description } ?? [:]
)
)
}
}
}
@@ -1190,16 +1256,20 @@ struct CollectEverythingLogHandler: LogHandler {
self.logStore = logStore
}
func log(level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
file: String, function: String, line: UInt) {
func log(
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
file: String,
function: String,
line: UInt
) {
self.logStore.append(level: level, message: message, metadata: self.metadata.merging(metadata ?? [:]) { $1 })
}
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get {
return self.metadata[key]
self.metadata[key]
}
set {
self.metadata[key] = newValue
@@ -1355,7 +1425,10 @@ class HTTPEchoHandler: ChannelInboundHandler {
let request = self.unwrapInboundIn(data)
switch request {
case .head(let requestHead):
context.writeAndFlush(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok, headers: requestHead.headers))), promise: nil)
context.writeAndFlush(
self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok, headers: requestHead.headers))),
promise: nil
)
case .body(let bytes):
context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(bytes))), promise: nil)
case .end:
@@ -1374,7 +1447,10 @@ final class HTTPEchoHeaders: ChannelInboundHandler {
let request = self.unwrapInboundIn(data)
switch request {
case .head(let requestHead):
context.writeAndFlush(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok, headers: requestHead.headers))), promise: nil)
context.writeAndFlush(
self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok, headers: requestHead.headers))),
promise: nil
)
case .body:
break
case .end:
@@ -1410,7 +1486,10 @@ final class HTTP200DelayedHandler: ChannelInboundHandler {
self.pendingBodyParts = pendingBodyParts - 1
} else {
self.pendingBodyParts = nil
context.writeAndFlush(self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok))), promise: nil)
context.writeAndFlush(
self.wrapOutboundOut(.head(.init(version: .http1_1, status: .ok))),
promise: nil
)
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
}
}
@@ -1421,51 +1500,51 @@ final class HTTP200DelayedHandler: ChannelInboundHandler {
}
private let cert = """
-----BEGIN CERTIFICATE-----
MIICmDCCAYACCQCPC8JDqMh1zzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1
czAgFw0xODEwMzExNTU1MjJaGA8yMTE4MTAwNzE1NTUyMlowDTELMAkGA1UEBhMC
dXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiC+TGmbSP/nWWN1tj
yNfnWCU5ATjtIOfdtP6ycx8JSeqkvyNXG21kNUn14jTTU8BglGL2hfVpCbMisUdb
d3LpP8unSsvlOWwORFOViSy4YljSNM/FNoMtavuITA/sEELYgjWkz2o/uHPZHud9
+JQwGJgqIlMa3mr2IaaUZlWN3D1u88bzJYhpt3YyxRy9+OEoOKy36KdWwhKzV3S8
kXb0Y1GbAo68jJ9RfzeLy290mIs9qG2y1CNXWO6sxf6B//LaalizZiCfzYAVKcNR
9oNYsEJc5KB/+DsAGTzR7mL+oiU4h/vwVb2GTDat5C+PFGi6j1ujxYTRPO538ljg
dslnAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFYhA7sw8odOsRO8/DUklBOjPnmn
a078oSumgPXXw6AgcoAJv/Qthjo6CCEtrjYfcA9jaBw9/Tii7mDmqDRS5c9ZPL8+
NEPdHjFCFBOEvlL6uHOgw0Z9Wz+5yCXnJ8oNUEgc3H2NbbzJF6sMBXSPtFS2NOK8
OsAI9OodMrDd6+lwljrmFoCCkJHDEfE637IcsbgFKkzhO/oNCRK6OrudG4teDahz
Au4LoEYwT730QKC/VQxxEVZobjn9/sTrq9CZlbPYHxX4fz6e00sX7H9i49vk9zQ5
5qCm9ljhrQPSa42Q62PPE2BEEGSP2KBm0J+H3vlvCD6+SNc/nMZjrRmgjrI=
-----END CERTIFICATE-----
"""
-----BEGIN CERTIFICATE-----
MIICmDCCAYACCQCPC8JDqMh1zzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1
czAgFw0xODEwMzExNTU1MjJaGA8yMTE4MTAwNzE1NTUyMlowDTELMAkGA1UEBhMC
dXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiC+TGmbSP/nWWN1tj
yNfnWCU5ATjtIOfdtP6ycx8JSeqkvyNXG21kNUn14jTTU8BglGL2hfVpCbMisUdb
d3LpP8unSsvlOWwORFOViSy4YljSNM/FNoMtavuITA/sEELYgjWkz2o/uHPZHud9
+JQwGJgqIlMa3mr2IaaUZlWN3D1u88bzJYhpt3YyxRy9+OEoOKy36KdWwhKzV3S8
kXb0Y1GbAo68jJ9RfzeLy290mIs9qG2y1CNXWO6sxf6B//LaalizZiCfzYAVKcNR
9oNYsEJc5KB/+DsAGTzR7mL+oiU4h/vwVb2GTDat5C+PFGi6j1ujxYTRPO538ljg
dslnAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFYhA7sw8odOsRO8/DUklBOjPnmn
a078oSumgPXXw6AgcoAJv/Qthjo6CCEtrjYfcA9jaBw9/Tii7mDmqDRS5c9ZPL8+
NEPdHjFCFBOEvlL6uHOgw0Z9Wz+5yCXnJ8oNUEgc3H2NbbzJF6sMBXSPtFS2NOK8
OsAI9OodMrDd6+lwljrmFoCCkJHDEfE637IcsbgFKkzhO/oNCRK6OrudG4teDahz
Au4LoEYwT730QKC/VQxxEVZobjn9/sTrq9CZlbPYHxX4fz6e00sX7H9i49vk9zQ5
5qCm9ljhrQPSa42Q62PPE2BEEGSP2KBm0J+H3vlvCD6+SNc/nMZjrRmgjrI=
-----END CERTIFICATE-----
"""
private let key = """
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDiC+TGmbSP/nWW
N1tjyNfnWCU5ATjtIOfdtP6ycx8JSeqkvyNXG21kNUn14jTTU8BglGL2hfVpCbMi
sUdbd3LpP8unSsvlOWwORFOViSy4YljSNM/FNoMtavuITA/sEELYgjWkz2o/uHPZ
Hud9+JQwGJgqIlMa3mr2IaaUZlWN3D1u88bzJYhpt3YyxRy9+OEoOKy36KdWwhKz
V3S8kXb0Y1GbAo68jJ9RfzeLy290mIs9qG2y1CNXWO6sxf6B//LaalizZiCfzYAV
KcNR9oNYsEJc5KB/+DsAGTzR7mL+oiU4h/vwVb2GTDat5C+PFGi6j1ujxYTRPO53
8ljgdslnAgMBAAECggEBANZNWFNAnYJ2R5xmVuo/GxFk68Ujd4i4TZpPYbhkk+QG
g8I0w5htlEQQkVHfZx2CpTvq8feuAH/YhlA5qeD5WaPwq26q5qsmyV6tQGDgb9lO
w85l6ySZDbwdVOJe2il/MSB6MclSKvTGNm59chJnfHYsmvY3HHq4qsc2F+tRKYMW
pY75LgEbaTUV69J3cbC1wAeVjv0q/krND+YkhYpTxNZhbazK/FHOCvY+zFu9fg0L
zpwbn5fb6wIvqG7tXp7koa3QMn64AXmO/fb5mBd8G2vBGYnxwb7Egwdg/3Dw+BXu
ynQLP7ixWsE2KNfR9Ce1i3YvEo6QDTv2340I3dntxkECgYEA9vdaL4PGyvEbpim4
kqz1vuug8Iq0nTVDo6jmgH1o+XdcIbW3imXtgi5zUJpj4oDD7/4aufiJZjG64i/v
phe11xeUvh5QNNOzeMymVDoJut97F97KKKTv7bG8Rpon/WzH2I0SoAkECCwmdWAJ
H3nvOCnXEkpbCqmIUvHVURPRDn8CgYEA6lCk3EzFQlbXs3Sj5op61R3Mscx7/35A
eGv5axzbENHt1so+s3Zvyyi1bo4VBcwnKVCvQjmTuLiqrc9VfX8XdbiTUNnEr2u3
992Ja6DEJTZ9gy5WiviwYnwU2HpjwOVNBb17T0NLoRHkDZ6iXj7NZgwizOki5p3j
/hS0pObSIRkCgYEAiEdOGNIarHoHy9VR6H5QzR2xHYssx2NRA8p8B4MsnhxjVqaz
tUcxnJiNQXkwjRiJBrGthdnD2ASxH4dcMsb6rMpyZcbMc5ouewZS8j9khx4zCqUB
4RPC4eMmBb+jOZEBZlnSYUUYWHokbrij0B61BsTvzUQCoQuUElEoaSkKP3kCgYEA
mwdqXHvK076jjo9w1drvtEu4IDc8H2oH++TsrEr2QiWzaDZ9z71f8BnqGNCW5jQS
AQrqOjXgIArGmqMgXB0Xh4LsrUS4Fpx9ptiD0JsYy8pGtuGUzvQFt9OC80ve7kSI
dnDMwj+zLUmqCrzXjuWcfpUu/UaPGeiDbZuDfcteYhkCgYBLyL5JY7Qd4gVQIhFX
7Sv3sNJN3KZCQHEzut7IwojaxgpuxiFvgsoXXuYolVCQp32oWbYcE2Yke+hOKsTE
sCMAWZiSGN2Nrfea730IYAXkUm8bpEd3VxDXEEv13nxVeQof+JGMdlkldFGaBRDU
oYQsPj00S3/GA9WDapwe81Wl2A==
-----END PRIVATE KEY-----
"""
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDiC+TGmbSP/nWW
N1tjyNfnWCU5ATjtIOfdtP6ycx8JSeqkvyNXG21kNUn14jTTU8BglGL2hfVpCbMi
sUdbd3LpP8unSsvlOWwORFOViSy4YljSNM/FNoMtavuITA/sEELYgjWkz2o/uHPZ
Hud9+JQwGJgqIlMa3mr2IaaUZlWN3D1u88bzJYhpt3YyxRy9+OEoOKy36KdWwhKz
V3S8kXb0Y1GbAo68jJ9RfzeLy290mIs9qG2y1CNXWO6sxf6B//LaalizZiCfzYAV
KcNR9oNYsEJc5KB/+DsAGTzR7mL+oiU4h/vwVb2GTDat5C+PFGi6j1ujxYTRPO53
8ljgdslnAgMBAAECggEBANZNWFNAnYJ2R5xmVuo/GxFk68Ujd4i4TZpPYbhkk+QG
g8I0w5htlEQQkVHfZx2CpTvq8feuAH/YhlA5qeD5WaPwq26q5qsmyV6tQGDgb9lO
w85l6ySZDbwdVOJe2il/MSB6MclSKvTGNm59chJnfHYsmvY3HHq4qsc2F+tRKYMW
pY75LgEbaTUV69J3cbC1wAeVjv0q/krND+YkhYpTxNZhbazK/FHOCvY+zFu9fg0L
zpwbn5fb6wIvqG7tXp7koa3QMn64AXmO/fb5mBd8G2vBGYnxwb7Egwdg/3Dw+BXu
ynQLP7ixWsE2KNfR9Ce1i3YvEo6QDTv2340I3dntxkECgYEA9vdaL4PGyvEbpim4
kqz1vuug8Iq0nTVDo6jmgH1o+XdcIbW3imXtgi5zUJpj4oDD7/4aufiJZjG64i/v
phe11xeUvh5QNNOzeMymVDoJut97F97KKKTv7bG8Rpon/WzH2I0SoAkECCwmdWAJ
H3nvOCnXEkpbCqmIUvHVURPRDn8CgYEA6lCk3EzFQlbXs3Sj5op61R3Mscx7/35A
eGv5axzbENHt1so+s3Zvyyi1bo4VBcwnKVCvQjmTuLiqrc9VfX8XdbiTUNnEr2u3
992Ja6DEJTZ9gy5WiviwYnwU2HpjwOVNBb17T0NLoRHkDZ6iXj7NZgwizOki5p3j
/hS0pObSIRkCgYEAiEdOGNIarHoHy9VR6H5QzR2xHYssx2NRA8p8B4MsnhxjVqaz
tUcxnJiNQXkwjRiJBrGthdnD2ASxH4dcMsb6rMpyZcbMc5ouewZS8j9khx4zCqUB
4RPC4eMmBb+jOZEBZlnSYUUYWHokbrij0B61BsTvzUQCoQuUElEoaSkKP3kCgYEA
mwdqXHvK076jjo9w1drvtEu4IDc8H2oH++TsrEr2QiWzaDZ9z71f8BnqGNCW5jQS
AQrqOjXgIArGmqMgXB0Xh4LsrUS4Fpx9ptiD0JsYy8pGtuGUzvQFt9OC80ve7kSI
dnDMwj+zLUmqCrzXjuWcfpUu/UaPGeiDbZuDfcteYhkCgYBLyL5JY7Qd4gVQIhFX
7Sv3sNJN3KZCQHEzut7IwojaxgpuxiFvgsoXXuYolVCQp32oWbYcE2Yke+hOKsTE
sCMAWZiSGN2Nrfea730IYAXkUm8bpEd3VxDXEEv13nxVeQof+JGMdlkldFGaBRDU
oYQsPj00S3/GA9WDapwe81Wl2A==
-----END PRIVATE KEY-----
"""
File diff suppressed because it is too large Load Diff
@@ -155,7 +155,8 @@ final class HTTPClientUncleanSSLConnectionShutdownTests: XCTestCase {
)
defer { XCTAssertNoThrow(try client.syncShutdown()) }
XCTAssertThrowsError(try client.get(url: "https://localhost:\(httpBin.port)/transferencodingtruncated").wait()) {
XCTAssertThrowsError(try client.get(url: "https://localhost:\(httpBin.port)/transferencodingtruncated").wait())
{
XCTAssertEqual($0 as? HTTPParserError, .invalidEOFState)
}
}
@@ -184,7 +185,7 @@ final class HTTPBinForSSLUncleanShutdown {
let serverChannel: Channel
var port: Int {
return Int(self.serverChannel.localAddress!.port!)
Int(self.serverChannel.localAddress!.port!)
}
init() {
@@ -231,61 +232,61 @@ private final class HTTPBinForSSLUncleanShutdownHandler: ChannelInboundHandler {
switch req.uri {
case "/nocontentlength":
response = """
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
\r\n\
foo
"""
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
\r\n\
foo
"""
case "/nocontent":
response = """
HTTP/1.1 204 OK\r\n\
Connection: close\r\n\
\r\n
"""
HTTP/1.1 204 OK\r\n\
Connection: close\r\n\
\r\n
"""
case "/noresponse":
response = nil
case "/wrongcontentlength":
response = """
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
Content-Length: 6\r\n\
\r\n\
foo
"""
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
Content-Length: 6\r\n\
\r\n\
foo
"""
case "/transferencoding":
response = """
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
3\r\n\
foo\r\n\
0\r\n\
\r\n
"""
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
3\r\n\
foo\r\n\
0\r\n\
\r\n
"""
case "/transferencodingtruncated":
response = """
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
12\r\n\
foo
"""
HTTP/1.1 200 OK\r\n\
Connection: close\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
12\r\n\
foo
"""
default:
response = """
HTTP/1.1 404 OK\r\n\
Connection: close\r\n\
Content-Length: 9\r\n\
\r\n\
Not Found
"""
HTTP/1.1 404 OK\r\n\
Connection: close\r\n\
Content-Length: 9\r\n\
\r\n\
Not Found
"""
}
if let response = response {
@@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOPosix
@@ -20,18 +19,22 @@ import NIOSOCKS
import NIOSSL
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPool_FactoryTests: XCTestCase {
func testConnectionCreationTimesoutIfDeadlineIsInThePast() {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
var server: Channel?
XCTAssertNoThrow(server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait())
XCTAssertNoThrow(
server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait()
)
defer {
XCTAssertNoThrow(try server?.close().wait())
}
@@ -45,13 +48,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
sslContextCache: .init()
)
XCTAssertThrowsError(try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() - .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
XCTAssertThrowsError(
try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() - .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
) {
XCTAssertEqual($0 as? HTTPClientError, .connectTimeout)
}
@@ -62,12 +66,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
var server: Channel?
XCTAssertNoThrow(server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait())
XCTAssertNoThrow(
server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait()
)
defer {
XCTAssertNoThrow(try server?.close().wait())
}
@@ -82,13 +88,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
sslContextCache: .init()
)
XCTAssertThrowsError(try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() + .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
XCTAssertThrowsError(
try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() + .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
) {
XCTAssertEqual($0 as? HTTPClientError, .socksHandshakeTimeout)
}
@@ -99,12 +106,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
var server: Channel?
XCTAssertNoThrow(server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait())
XCTAssertNoThrow(
server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait()
)
defer {
XCTAssertNoThrow(try server?.close().wait())
}
@@ -119,13 +128,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
sslContextCache: .init()
)
XCTAssertThrowsError(try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() + .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
XCTAssertThrowsError(
try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() + .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
) {
XCTAssertEqual($0 as? HTTPClientError, .httpProxyHandshakeTimeout)
}
@@ -136,12 +146,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
var server: Channel?
XCTAssertNoThrow(server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait())
XCTAssertNoThrow(
server = try ServerBootstrap(group: group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(NeverrespondServerHandler())
}
.bind(to: .init(ipAddress: "127.0.0.1", port: 0))
.wait()
)
defer {
XCTAssertNoThrow(try server?.close().wait())
}
@@ -158,13 +170,14 @@ class HTTPConnectionPool_FactoryTests: XCTestCase {
sslContextCache: .init()
)
XCTAssertThrowsError(try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() + .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
XCTAssertThrowsError(
try factory.makeChannel(
requester: ExplodingRequester(),
connectionID: 1,
deadline: .now() + .seconds(1),
eventLoop: group.next(),
logger: .init(label: "test")
).wait()
) {
XCTAssertEqual($0 as? HTTPClientError, .tlsHandshakeTimeout)
}
@@ -12,15 +12,20 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testCreatingConnections() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -52,7 +57,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testCreatingConnectionAndFailing() {
let elg = EmbeddedEventLoopGroup(loops: 4)
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -103,7 +112,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
let el3 = elg.next()
let el4 = elg.next()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
for el in [el1, el2, el3, el4] {
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
@@ -130,7 +143,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
let el4 = elg.next()
let el5 = elg.next()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
for el in [el1, el2, el3, el4] {
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
@@ -157,7 +174,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
let el4 = elg.next()
let el5 = elg.next()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
for el in [el1, el2, el3, el4] {
XCTAssertEqual(connections.startingGeneralPurposeConnections, 0)
@@ -181,7 +202,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
let el1 = elg.next()
let el2 = elg.next()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
for el in [el1, el1, el1, el1, el2] {
let connID = connections.createNewConnection(on: el)
@@ -228,7 +253,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testCloseConnectionIfIdle() {
let elg = EmbeddedEventLoopGroup(loops: 1)
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
let el1 = elg.next()
@@ -248,7 +277,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testCloseConnectionIfIdleButLeasedRaceCondition() {
let elg = EmbeddedEventLoopGroup(loops: 1)
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
let el1 = elg.next()
@@ -267,7 +300,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testCloseConnectionIfIdleButClosedRaceCondition() {
let elg = EmbeddedEventLoopGroup(loops: 1)
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
let el1 = elg.next()
@@ -288,7 +325,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
let el3 = elg.next()
let el4 = elg.next()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: .init(), maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: .init(),
maximumConnectionUses: nil
)
for el in [el1, el2, el3, el4] {
let connID = connections.createNewConnection(on: el)
@@ -343,7 +384,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -372,7 +417,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2WithPendingRequestsWithRequiredEventLoop() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -411,7 +460,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2WithPendingRequestsWithRequiredEventLoopSameAsStartingConnections() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -439,7 +492,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2WithPendingRequestsWithPreferredEventLoop() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -478,7 +535,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2WithAlreadyLeasedHTTP1Connection() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
let el3 = elg.next()
@@ -522,7 +583,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2WithMoreStartingConnectionsThanMaximumAllowedConccurentConnections() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 2, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 2,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -557,7 +622,11 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
func testMigrationFromHTTP2StartsEnoghOverflowConnectionsForRequiredEventLoopRequests() {
let elg = EmbeddedEventLoopGroup(loops: 4)
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 1, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 1,
generator: generator,
maximumConnectionUses: nil
)
let el1 = elg.next()
let el2 = elg.next()
@@ -599,16 +668,23 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
let el2 = elg.next()
let generator = HTTPConnectionPool.Connection.ID.Generator()
var connections = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: generator, maximumConnectionUses: nil)
var connections = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: generator,
maximumConnectionUses: nil
)
let connID1 = connections.createNewConnection(on: el1)
let context = connections.migrateToHTTP2()
XCTAssertEqual(context, .init(
backingOff: [],
starting: [(connID1, el1)],
close: []
))
XCTAssertEqual(
context,
.init(
backingOff: [],
starting: [(connID1, el1)],
close: []
)
)
let connID2 = generator.next()
@@ -626,8 +702,7 @@ class HTTPConnectionPool_HTTP1ConnectionsTests: XCTestCase {
extension HTTPConnectionPool.HTTP1Connections.HTTP1ToHTTP2MigrationContext: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.close == rhs.close &&
lhs.starting.elementsEqual(rhs.starting, by: { $0.0 == $1.0 && $0.1 === $1.1 }) &&
lhs.backingOff.elementsEqual(rhs.backingOff, by: { $0.0 == $1.0 && $0.1 === $1.1 })
lhs.close == rhs.close && lhs.starting.elementsEqual(rhs.starting, by: { $0.0 == $1.0 && $0.1 === $1.1 })
&& lhs.backingOff.elementsEqual(rhs.backingOff, by: { $0.0 == $1.0 && $0.1 === $1.1 })
}
}
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOPosix
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
func testCreatingAndFailingConnections() {
struct SomeError: Error, Equatable {}
@@ -197,9 +198,12 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = action.connection else {
return XCTFail("Unexpected connection action: \(action.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
let failedConnect1 = state.failedToCreateNewConnection(HTTPClientError.connectTimeout, connectionID: connectionID)
let failedConnect1 = state.failedToCreateNewConnection(
HTTPClientError.connectTimeout,
connectionID: connectionID
)
XCTAssertEqual(failedConnect1.request, .none)
guard case .scheduleBackoffTimer(connectionID, let backoffTimeAmount1, _) = failedConnect1.connection else {
return XCTFail("Unexpected connection action: \(failedConnect1.connection)")
@@ -212,9 +216,12 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
return XCTFail("Unexpected connection action: \(backoffDoneAction.connection)")
}
XCTAssertGreaterThan(newConnectionID, connectionID)
XCTAssert(connectionEL === newEventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === newEventLoop) // XCTAssertIdentical not available on Linux
let failedConnect2 = state.failedToCreateNewConnection(HTTPClientError.connectTimeout, connectionID: newConnectionID)
let failedConnect2 = state.failedToCreateNewConnection(
HTTPClientError.connectTimeout,
connectionID: newConnectionID
)
XCTAssertEqual(failedConnect2.request, .none)
guard case .scheduleBackoffTimer(newConnectionID, let backoffTimeAmount2, _) = failedConnect2.connection else {
return XCTFail("Unexpected connection action: \(failedConnect2.connection)")
@@ -227,7 +234,9 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
guard case .failRequest(let requestToFail, let requestError, cancelTimeout: false) = failRequest.request else {
return XCTFail("Unexpected request action: \(action.request)")
}
XCTAssert(requestToFail.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
// XCTAssertIdentical not available on Linux
XCTAssert(requestToFail.__testOnly_wrapped_request() === mockRequest)
XCTAssertEqual(requestError as? HTTPClientError, .connectTimeout)
XCTAssertEqual(failRequest.connection, .none)
@@ -257,7 +266,7 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = executeAction.connection else {
return XCTFail("Unexpected connection action: \(executeAction.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
// 2. cancel request
@@ -269,7 +278,9 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(state.timeoutRequest(request.id), .none, "To late timeout is ignored")
// 4. succeed connection attempt
let connectedAction = state.newHTTP1ConnectionCreated(.__testOnly_connection(id: connectionID, eventLoop: connectionEL))
let connectedAction = state.newHTTP1ConnectionCreated(
.__testOnly_connection(id: connectionID, eventLoop: connectionEL)
)
XCTAssertEqual(connectedAction.request, .none, "Request must not be executed")
XCTAssertEqual(connectedAction.connection, .scheduleTimeoutTimer(connectionID, on: connectionEL))
}
@@ -296,15 +307,18 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = executeAction.connection else {
return XCTFail("Unexpected connection action: \(executeAction.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
// 2. connection succeeds
let connection: HTTPConnectionPool.Connection = .__testOnly_connection(id: connectionID, eventLoop: connectionEL)
let connection: HTTPConnectionPool.Connection = .__testOnly_connection(
id: connectionID,
eventLoop: connectionEL
)
let connectedAction = state.newHTTP1ConnectionCreated(connection)
guard case .executeRequest(request, connection, cancelTimeout: true) = connectedAction.request else {
return XCTFail("Unexpected request action: \(connectedAction.request)")
}
XCTAssert(request.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
XCTAssert(request.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
XCTAssertEqual(connectedAction.connection, .none)
// 3. shutdown
@@ -324,7 +338,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
let finalRequest = HTTPConnectionPool.Request(finalMockRequest)
let failAction = state.executeRequest(finalRequest)
XCTAssertEqual(failAction.connection, .none)
XCTAssertEqual(failAction.request, .failRequest(finalRequest, HTTPClientError.alreadyShutdown, cancelTimeout: false))
XCTAssertEqual(
failAction.request,
.failRequest(finalRequest, HTTPClientError.alreadyShutdown, cancelTimeout: false)
)
// 5. close open connection
let closeAction = state.http1ConnectionClosed(connectionID)
@@ -345,7 +362,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
// Add eight requests to fill all connections
for _ in 0..<8 {
let eventLoop = elg.next()
guard let expectedConnection = connections.newestParkedConnection(for: eventLoop) ?? connections.newestParkedConnection else {
guard
let expectedConnection = connections.newestParkedConnection(for: eventLoop)
?? connections.newestParkedConnection
else {
return XCTFail("Expected to still have connections available")
}
@@ -354,7 +374,8 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
let action = state.executeRequest(request)
XCTAssertEqual(action.connection, .cancelTimeoutTimer(expectedConnection.id))
guard case .executeRequest(let returnedRequest, expectedConnection, cancelTimeout: false) = action.request else {
guard case .executeRequest(let returnedRequest, expectedConnection, cancelTimeout: false) = action.request
else {
return XCTFail("Expected to execute a request next, but got: \(action.request)")
}
@@ -428,7 +449,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
// 10% of the cases enforce the eventLoop
let elRequired = (0..<10).randomElement().flatMap { $0 == 0 ? true : false }!
let mockRequest = MockHTTPScheduableRequest(eventLoop: reqEventLoop, requiresEventLoopForChannel: elRequired)
let mockRequest = MockHTTPScheduableRequest(
eventLoop: reqEventLoop,
requiresEventLoopForChannel: elRequired
)
let request = HTTPConnectionPool.Request(mockRequest)
let action = state.executeRequest(request)
@@ -440,7 +464,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssert(connEventLoop === reqEventLoop)
XCTAssertEqual(action.request, .scheduleRequestTimeout(for: request, on: reqEventLoop))
let connection: HTTPConnectionPool.Connection = .__testOnly_connection(id: connectionID, eventLoop: connEventLoop)
let connection: HTTPConnectionPool.Connection = .__testOnly_connection(
id: connectionID,
eventLoop: connEventLoop
)
let createdAction = state.newHTTP1ConnectionCreated(connection)
XCTAssertEqual(createdAction.request, .executeRequest(request, connection, cancelTimeout: true))
XCTAssertEqual(createdAction.connection, .none)
@@ -451,7 +478,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(state.http1ConnectionClosed(connectionID), .none)
case .cancelTimeoutTimer(let connectionID):
guard let expectedConnection = connections.newestParkedConnection(for: reqEventLoop) ?? connections.newestParkedConnection else {
guard
let expectedConnection = connections.newestParkedConnection(for: reqEventLoop)
?? connections.newestParkedConnection
else {
return XCTFail("Expected to have connections available")
}
@@ -459,7 +489,11 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssert(expectedConnection.eventLoop === reqEventLoop)
}
XCTAssertEqual(connectionID, expectedConnection.id, "Request is scheduled on the connection we expected")
XCTAssertEqual(
connectionID,
expectedConnection.id,
"Request is scheduled on the connection we expected"
)
XCTAssertNoThrow(try connections.activateConnection(connectionID))
guard case .executeRequest(let request, let connection, cancelTimeout: false) = action.request else {
@@ -469,8 +503,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertNoThrow(try connections.execute(request.__testOnly_wrapped_request(), on: connection))
XCTAssertNoThrow(try connections.finishExecution(connection.id))
XCTAssertEqual(state.http1ConnectionReleased(connection.id),
.init(request: .none, connection: .scheduleTimeoutTimer(connection.id, on: connection.eventLoop)))
XCTAssertEqual(
state.http1ConnectionReleased(connection.id),
.init(request: .none, connection: .scheduleTimeoutTimer(connection.id, on: connection.eventLoop))
)
XCTAssertNoThrow(try connections.parkConnection(connectionID))
default:
@@ -542,7 +578,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
// Add eight requests to fill all connections
for _ in 0..<8 {
let eventLoop = elg.next()
guard let expectedConnection = connections.newestParkedConnection(for: eventLoop) ?? connections.newestParkedConnection else {
guard
let expectedConnection = connections.newestParkedConnection(for: eventLoop)
?? connections.newestParkedConnection
else {
return XCTFail("Expected to still have connections available")
}
@@ -589,12 +628,20 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
guard let newConnection = maybeNewConnection else { return XCTFail("Expected to get a new connection") }
let afterRecreationAction = state.newHTTP1ConnectionCreated(newConnection)
XCTAssertEqual(afterRecreationAction.connection, .none)
guard case .executeRequest(let request, newConnection, cancelTimeout: true) = afterRecreationAction.request else {
guard
case .executeRequest(let request, newConnection, cancelTimeout: true) = afterRecreationAction
.request
else {
return XCTFail("Unexpected request action: \(action.request)")
}
XCTAssertEqual(request.id, queuedRequestsOrder.popFirst())
XCTAssertNoThrow(try connections.execute(queuer.get(request.id, request: request.__testOnly_wrapped_request()), on: newConnection))
XCTAssertNoThrow(
try connections.execute(
queuer.get(request.id, request: request.__testOnly_wrapped_request()),
on: newConnection
)
)
case .none:
XCTAssert(queuer.isEmpty)
@@ -730,7 +777,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(executeAction.request, .scheduleRequestTimeout(for: request, on: mockRequest.eventLoop))
let failAction = state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID)
let failAction = state.failedToCreateNewConnection(
HTTPClientError.httpProxyHandshakeTimeout,
connectionID: connectionID
)
guard case .scheduleBackoffTimer(connectionID, backoff: _, on: let timerEL) = failAction.connection else {
return XCTFail("Expected to create a backoff timer")
}
@@ -738,7 +788,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(failAction.request, .none)
let timeoutAction = state.timeoutRequest(request.id)
XCTAssertEqual(timeoutAction.request, .failRequest(request, HTTPClientError.httpProxyHandshakeTimeout, cancelTimeout: false))
XCTAssertEqual(
timeoutAction.request,
.failRequest(request, HTTPClientError.httpProxyHandshakeTimeout, cancelTimeout: false)
)
XCTAssertEqual(timeoutAction.connection, .none)
}
@@ -764,7 +817,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(executeAction.request, .scheduleRequestTimeout(for: request, on: mockRequest.eventLoop))
let timeoutAction = state.timeoutRequest(request.id)
XCTAssertEqual(timeoutAction.request, .failRequest(request, HTTPClientError.connectTimeout, cancelTimeout: false))
XCTAssertEqual(
timeoutAction.request,
.failRequest(request, HTTPClientError.connectTimeout, cancelTimeout: false)
)
XCTAssertEqual(timeoutAction.connection, .none)
}
@@ -802,7 +858,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(executeAction2.request, .scheduleRequestTimeout(for: request2, on: connEL1))
let failAction = state.failedToCreateNewConnection(HTTPClientError.httpProxyHandshakeTimeout, connectionID: connectionID1)
let failAction = state.failedToCreateNewConnection(
HTTPClientError.httpProxyHandshakeTimeout,
connectionID: connectionID1
)
guard case .scheduleBackoffTimer(connectionID1, backoff: _, on: let timerEL) = failAction.connection else {
return XCTFail("Expected to create a backoff timer")
}
@@ -816,7 +875,10 @@ class HTTPConnectionPool_HTTP1StateMachineTests: XCTestCase {
XCTAssertEqual(createdAction.connection, .none)
let timeoutAction = state.timeoutRequest(request2.id)
XCTAssertEqual(timeoutAction.request, .failRequest(request2, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false))
XCTAssertEqual(
timeoutAction.request,
.failRequest(request2, HTTPClientError.getConnectionFromPoolTimeout, cancelTimeout: false)
)
XCTAssertEqual(timeoutAction.connection, .none)
}
}
@@ -12,11 +12,12 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
func testCreatingConnections() {
let elg = EmbeddedEventLoopGroup(loops: 4)
@@ -32,7 +33,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertEqual(conn1CreatedContext.isIdle, true)
XCTAssert(conn1CreatedContext.eventLoop === el1)
@@ -46,7 +50,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
let conn2ID = connections.createNewConnection(on: el2)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el2))
let conn2: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn2ID, eventLoop: el2)
let (conn2Index, conn2CreatedContext) = connections.newHTTP2ConnectionEstablished(conn2, maxConcurrentStreams: 100)
let (conn2Index, conn2CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn2,
maxConcurrentStreams: 100
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
XCTAssertTrue(conn1CreatedContext.isIdle)
XCTAssert(conn2CreatedContext.eventLoop === el2)
@@ -83,7 +90,9 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
XCTAssert(conn1FailContext.eventLoop === el1)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
XCTAssertFalse(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests(for: el1))
let (replaceConn1ID, replaceConn1EL) = connections.createNewConnectionByReplacingClosedConnection(at: conn1FailIndex)
let (replaceConn1ID, replaceConn1EL) = connections.createNewConnectionByReplacingClosedConnection(
at: conn1FailIndex
)
XCTAssert(replaceConn1EL === el1)
XCTAssertEqual(replaceConn1ID, 1)
XCTAssertTrue(connections.hasConnectionThatCanOrWillBeAbleToExecuteRequests)
@@ -336,13 +345,19 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 100)
XCTAssertEqual(leasedConn1, conn1)
XCTAssertEqual(leasdConnContext1.wasIdle, true)
XCTAssertNil(connections.leaseStream(onRequired: el1), "should not be able to lease stream because they are all already leased")
XCTAssertNil(
connections.leaseStream(onRequired: el1),
"should not be able to lease stream because they are all already leased"
)
let (_, releaseContext) = connections.releaseStream(conn1ID)
XCTAssertFalse(releaseContext.isIdle)
@@ -354,7 +369,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
XCTAssertEqual(leasedConn, conn1)
XCTAssertEqual(leaseContext.wasIdle, false)
XCTAssertNil(connections.leaseStream(onRequired: el1), "should not be able to lease stream because they are all already leased")
XCTAssertNil(
connections.leaseStream(onRequired: el1),
"should not be able to lease stream because they are all already leased"
)
}
func testGoAway() {
@@ -364,7 +382,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 10)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 10
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 10)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 2)
@@ -386,7 +407,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
)
)
XCTAssertNil(connections.leaseStream(onRequired: el1), "we should not be able to lease a stream because the connection is draining")
XCTAssertNil(
connections.leaseStream(onRequired: el1),
"we should not be able to lease a stream because the connection is draining"
)
// a server can potentially send more than one connection go away and we should not crash
XCTAssertTrue(connections.goAwayReceived(conn1ID)?.eventLoop === el1)
@@ -445,7 +469,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 1
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
@@ -454,7 +481,8 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
XCTAssertNil(connections.leaseStream(onRequired: el1), "all streams are in use")
guard let (_, newSettingsContext1) = connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 2) else {
guard let (_, newSettingsContext1) = connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 2)
else {
return XCTFail("Expected to get a new settings context")
}
XCTAssertEqual(newSettingsContext1.availableStreams, 1)
@@ -467,7 +495,8 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
XCTAssertEqual(leasedConn2, conn1)
XCTAssertEqual(leaseContext2.wasIdle, false)
guard let (_, newSettingsContext2) = connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 1) else {
guard let (_, newSettingsContext2) = connections.newHTTP2MaxConcurrentStreamsReceived(conn1ID, newMaxStreams: 1)
else {
return XCTFail("Expected to get a new settings context")
}
XCTAssertEqual(newSettingsContext2.availableStreams, 0)
@@ -500,7 +529,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 1
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
@@ -535,7 +567,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
let conn1ID = connections.createNewConnection(on: el1)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 1
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 1)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 1)
XCTAssertEqual(leasedConn1, conn1)
@@ -556,9 +591,11 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
starting: [(conn1ID, el1)],
backingOff: [(conn2ID, el2)]
)
XCTAssertTrue(connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2]
).isEmpty)
XCTAssertTrue(
connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2]
).isEmpty
)
XCTAssertEqual(
connections.stats,
@@ -574,7 +611,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 2)
@@ -615,7 +655,10 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
)
let conn1: HTTPConnectionPool.Connection = .__testOnly_connection(id: conn1ID, eventLoop: el1)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(conn1, maxConcurrentStreams: 100)
let (conn1Index, conn1CreatedContext) = connections.newHTTP2ConnectionEstablished(
conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(conn1CreatedContext.availableStreams, 100)
let (leasedConn1, leasdConnContext1) = connections.leaseStreams(at: conn1Index, count: 2)
@@ -714,9 +757,12 @@ class HTTPConnectionPool_HTTP2ConnectionsTests: XCTestCase {
backingOff: [(conn3ID, el3)]
)
XCTAssertTrue(connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2, el3]
).isEmpty, "we still have an active connection for el1 and should not create a new one")
XCTAssertTrue(
connections.createConnectionsAfterMigrationIfNeeded(
requiredEventLoopsOfPendingRequests: [el1, el2, el3]
).isEmpty,
"we still have an active connection for el1 and should not create a new one"
)
guard let (leasedConn, _) = connections.leaseStream(onRequired: el1) else {
return XCTFail("could not lease stream on el1")
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOPosix
import XCTest
@testable import AsyncHTTPClient
private typealias Action = HTTPConnectionPool.StateMachine.Action
private typealias ConnectionAction = HTTPConnectionPool.StateMachine.ConnectionAction
private typealias RequestAction = HTTPConnectionPool.StateMachine.RequestAction
@@ -127,14 +128,17 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
/// shutdown should only close one connection
let shutdownAction = state.shutdown()
XCTAssertEqual(shutdownAction.request, .none)
XCTAssertEqual(shutdownAction.connection, .cleanupConnections(
.init(
close: [conn],
cancel: [],
connectBackoff: []
),
isShutdown: .yes(unclean: false)
))
XCTAssertEqual(
shutdownAction.connection,
.cleanupConnections(
.init(
close: [conn],
cancel: [],
connectBackoff: []
),
isShutdown: .yes(unclean: false)
)
)
}
func testConnectionFailureBackoff() {
@@ -158,9 +162,12 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = action.connection else {
return XCTFail("Unexpected connection action: \(action.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
let failedConnect1 = state.failedToCreateNewConnection(HTTPClientError.connectTimeout, connectionID: connectionID)
let failedConnect1 = state.failedToCreateNewConnection(
HTTPClientError.connectTimeout,
connectionID: connectionID
)
XCTAssertEqual(failedConnect1.request, .none)
guard case .scheduleBackoffTimer(connectionID, let backoffTimeAmount1, _) = failedConnect1.connection else {
return XCTFail("Unexpected connection action: \(failedConnect1.connection)")
@@ -173,9 +180,12 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
return XCTFail("Unexpected connection action: \(backoffDoneAction.connection)")
}
XCTAssertGreaterThan(newConnectionID, connectionID)
XCTAssert(connectionEL === newEventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === newEventLoop) // XCTAssertIdentical not available on Linux
let failedConnect2 = state.failedToCreateNewConnection(HTTPClientError.connectTimeout, connectionID: newConnectionID)
let failedConnect2 = state.failedToCreateNewConnection(
HTTPClientError.connectTimeout,
connectionID: newConnectionID
)
XCTAssertEqual(failedConnect2.request, .none)
guard case .scheduleBackoffTimer(newConnectionID, let backoffTimeAmount2, _) = failedConnect2.connection else {
return XCTFail("Unexpected connection action: \(failedConnect2.connection)")
@@ -188,7 +198,8 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .failRequest(let requestToFail, let requestError, cancelTimeout: false) = failRequest.request else {
return XCTFail("Unexpected request action: \(action.request)")
}
XCTAssert(requestToFail.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
// XCTAssertIdentical not available on Linux
XCTAssert(requestToFail.__testOnly_wrapped_request() === mockRequest)
XCTAssertEqual(requestError as? HTTPClientError, .connectTimeout)
XCTAssertEqual(failRequest.connection, .none)
@@ -218,7 +229,7 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = action.connection else {
return XCTFail("Unexpected connection action: \(action.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
// 2. initialise shutdown
let shutdownAction = state.shutdown()
@@ -257,11 +268,12 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = action.connection else {
return XCTFail("Unexpected connection action: \(action.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
let failedConnectAction = state.failedToCreateNewConnection(SomeError(), connectionID: connectionID)
XCTAssertEqual(failedConnectAction.connection, .none)
guard case .failRequestsAndCancelTimeouts(let requestsToFail, let requestError) = failedConnectAction.request else {
guard case .failRequestsAndCancelTimeouts(let requestsToFail, let requestError) = failedConnectAction.request
else {
return XCTFail("Unexpected request action: \(action.request)")
}
XCTAssertEqualTypeAndValue(requestError, SomeError())
@@ -289,7 +301,7 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = executeAction.connection else {
return XCTFail("Unexpected connection action: \(executeAction.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
// 2. cancel request
let cancelAction = state.cancelRequest(request.id)
@@ -329,15 +341,18 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .createConnection(let connectionID, on: let connectionEL) = executeAction.connection else {
return XCTFail("Unexpected connection action: \(executeAction.connection)")
}
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
XCTAssert(connectionEL === mockRequest.eventLoop) // XCTAssertIdentical not available on Linux
// 2. connection succeeds
let connection: HTTPConnectionPool.Connection = .__testOnly_connection(id: connectionID, eventLoop: connectionEL)
let connection: HTTPConnectionPool.Connection = .__testOnly_connection(
id: connectionID,
eventLoop: connectionEL
)
let connectedAction = state.newHTTP2ConnectionEstablished(connection, maxConcurrentStreams: 100)
guard case .executeRequestsAndCancelTimeouts([request], connection) = connectedAction.request else {
return XCTFail("Unexpected request action: \(connectedAction.request)")
}
XCTAssert(request.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
XCTAssert(request.__testOnly_wrapped_request() === mockRequest) // XCTAssertIdentical not available on Linux
XCTAssertEqual(connectedAction.connection, .none)
// 3. shutdown
@@ -357,7 +372,10 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
let finalRequest = HTTPConnectionPool.Request(finalMockRequest)
let failAction = state.executeRequest(finalRequest)
XCTAssertEqual(failAction.connection, .none)
XCTAssertEqual(failAction.request, .failRequest(finalRequest, HTTPClientError.alreadyShutdown, cancelTimeout: false))
XCTAssertEqual(
failAction.request,
.failRequest(finalRequest, HTTPClientError.alreadyShutdown, cancelTimeout: false)
)
// 5. close open connection
let closeAction = state.http2ConnectionClosed(connectionID)
@@ -416,7 +434,10 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
newHTTP2Connection: conn2,
maxConcurrentStreams: 100
)
XCTAssertEqual(http2ConnectAction.connection, .migration(createConnections: [], closeConnections: [], scheduleTimeout: nil))
XCTAssertEqual(
http2ConnectAction.connection,
.migration(createConnections: [], closeConnections: [], scheduleTimeout: nil)
)
guard case .executeRequestsAndCancelTimeouts([request2], conn2) = http2ConnectAction.request else {
return XCTFail("Unexpected request action \(http2ConnectAction.request)")
}
@@ -428,11 +449,17 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
let shutdownAction = http2State.shutdown()
XCTAssertEqual(shutdownAction.request, .none)
XCTAssertEqual(shutdownAction.connection, .cleanupConnections(.init(
close: [conn2],
cancel: [],
connectBackoff: []
), isShutdown: .no))
XCTAssertEqual(
shutdownAction.connection,
.cleanupConnections(
.init(
close: [conn2],
cancel: [],
connectBackoff: []
),
isShutdown: .no
)
)
let releaseAction = http2State.http1ConnectionReleased(conn1ID)
XCTAssertEqual(releaseAction.request, .none)
@@ -445,7 +472,11 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// establish one idle http2 connection
let idGenerator = HTTPConnectionPool.Connection.ID.Generator()
var http1Conns = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: idGenerator, maximumConnectionUses: nil)
var http1Conns = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: idGenerator,
maximumConnectionUses: nil
)
let conn1ID = http1Conns.createNewConnection(on: el1)
var state = HTTPConnectionPool.HTTP2StateMachine(
idGenerator: idGenerator,
@@ -455,14 +486,22 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
)
let conn1 = HTTPConnectionPool.Connection.__testOnly_connection(id: conn1ID, eventLoop: el1)
let connectAction = state.migrateFromHTTP1(http1Connections: http1Conns, requests: .init(), newHTTP2Connection: conn1, maxConcurrentStreams: 100)
let connectAction = state.migrateFromHTTP1(
http1Connections: http1Conns,
requests: .init(),
newHTTP2Connection: conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(connectAction.request, .none)
XCTAssertEqual(connectAction.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
))
XCTAssertEqual(
connectAction.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
)
)
// execute request on idle connection
let mockRequest1 = MockHTTPScheduableRequest(eventLoop: el1)
@@ -495,7 +534,11 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// establish one idle http2 connection
let idGenerator = HTTPConnectionPool.Connection.ID.Generator()
var http1Conns = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: idGenerator, maximumConnectionUses: nil)
var http1Conns = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: idGenerator,
maximumConnectionUses: nil
)
let conn1ID = http1Conns.createNewConnection(on: el1)
var state = HTTPConnectionPool.HTTP2StateMachine(
idGenerator: idGenerator,
@@ -505,13 +548,21 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
)
let conn1 = HTTPConnectionPool.Connection.__testOnly_connection(id: conn1ID, eventLoop: el1)
let connectAction = state.migrateFromHTTP1(http1Connections: http1Conns, requests: .init(), newHTTP2Connection: conn1, maxConcurrentStreams: 100)
let connectAction = state.migrateFromHTTP1(
http1Connections: http1Conns,
requests: .init(),
newHTTP2Connection: conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(connectAction.request, .none)
XCTAssertEqual(connectAction.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
))
XCTAssertEqual(
connectAction.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
)
)
// let the connection timeout
let timeoutAction = state.connectionIdleTimeout(conn1ID)
@@ -528,7 +579,11 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// establish one idle http2 connection
let idGenerator = HTTPConnectionPool.Connection.ID.Generator()
var http1Conns = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: idGenerator, maximumConnectionUses: nil)
var http1Conns = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: idGenerator,
maximumConnectionUses: nil
)
let conn1ID = http1Conns.createNewConnection(on: el1)
var state = HTTPConnectionPool.HTTP2StateMachine(
idGenerator: idGenerator,
@@ -537,13 +592,21 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
maximumConnectionUses: nil
)
let conn1 = HTTPConnectionPool.Connection.__testOnly_connection(id: conn1ID, eventLoop: el1)
let connectAction = state.migrateFromHTTP1(http1Connections: http1Conns, requests: .init(), newHTTP2Connection: conn1, maxConcurrentStreams: 100)
let connectAction = state.migrateFromHTTP1(
http1Connections: http1Conns,
requests: .init(),
newHTTP2Connection: conn1,
maxConcurrentStreams: 100
)
XCTAssertEqual(connectAction.request, .none)
XCTAssertEqual(connectAction.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
))
XCTAssertEqual(
connectAction.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
)
)
// create new http2 connection
let mockRequest1 = MockHTTPScheduableRequest(eventLoop: el2, requiresEventLoopForChannel: true)
@@ -568,7 +631,11 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// establish one idle http2 connection
let idGenerator = HTTPConnectionPool.Connection.ID.Generator()
var http1Conns = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: idGenerator, maximumConnectionUses: nil)
var http1Conns = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: idGenerator,
maximumConnectionUses: nil
)
let conn1ID = http1Conns.createNewConnection(on: el1)
var state = HTTPConnectionPool.HTTP2StateMachine(
idGenerator: idGenerator,
@@ -586,11 +653,14 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
maxConcurrentStreams: 100
)
XCTAssertEqual(connectAction.request, .none)
XCTAssertEqual(connectAction.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
))
XCTAssertEqual(
connectAction.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
)
)
let goAwayAction = state.http2ConnectionGoAwayReceived(conn1ID)
XCTAssertEqual(goAwayAction.request, .none)
@@ -603,7 +673,11 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// establish one idle http2 connection
let idGenerator = HTTPConnectionPool.Connection.ID.Generator()
var http1Conns = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: idGenerator, maximumConnectionUses: nil)
var http1Conns = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: idGenerator,
maximumConnectionUses: nil
)
let conn1ID = http1Conns.createNewConnection(on: el1)
var state = HTTPConnectionPool.HTTP2StateMachine(
idGenerator: idGenerator,
@@ -620,11 +694,14 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
maxConcurrentStreams: 100
)
XCTAssertEqual(connectAction.request, .none)
XCTAssertEqual(connectAction.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
))
XCTAssertEqual(
connectAction.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
)
)
// execute request on idle connection
let mockRequest1 = MockHTTPScheduableRequest(eventLoop: el1)
@@ -649,7 +726,11 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// establish one idle http2 connection
let idGenerator = HTTPConnectionPool.Connection.ID.Generator()
var http1Conns = HTTPConnectionPool.HTTP1Connections(maximumConcurrentConnections: 8, generator: idGenerator, maximumConnectionUses: nil)
var http1Conns = HTTPConnectionPool.HTTP1Connections(
maximumConcurrentConnections: 8,
generator: idGenerator,
maximumConnectionUses: nil
)
let conn1ID = http1Conns.createNewConnection(on: el1)
var state = HTTPConnectionPool.HTTP2StateMachine(
idGenerator: idGenerator,
@@ -666,11 +747,14 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
maxConcurrentStreams: 1
)
XCTAssertEqual(connectAction1.request, .none)
XCTAssertEqual(connectAction1.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
))
XCTAssertEqual(
connectAction1.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: (conn1ID, el1)
)
)
// execute request
let mockRequest1 = MockHTTPScheduableRequest(eventLoop: el1)
@@ -770,11 +854,14 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
XCTAssertNoThrow(try connections.execute(request.__testOnly_wrapped_request(), on: conn1))
}
XCTAssertEqual(migrationAction.connection, .migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: nil
))
XCTAssertEqual(
migrationAction.connection,
.migration(
createConnections: [],
closeConnections: [],
scheduleTimeout: nil
)
)
/// remaining connections should be closed immediately without executing any request
for connID in connectionIDs.dropFirst() {
@@ -933,7 +1020,10 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .executeRequestsAndCancelTimeouts(let requests, let conn) = migrationAction.request else {
return XCTFail("unexpected request action \(migrationAction.request)")
}
XCTAssertEqual(migrationAction.connection, .migration(createConnections: [], closeConnections: [], scheduleTimeout: nil))
XCTAssertEqual(
migrationAction.connection,
.migration(createConnections: [], closeConnections: [], scheduleTimeout: nil)
)
XCTAssertEqual(conn, http2Conn)
XCTAssertEqual(requests.count, 10)
@@ -1030,14 +1120,20 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
}
// a request with new required event loop should create a new connection
let mockRequestWithRequiredEventLoop = MockHTTPScheduableRequest(eventLoop: el2, requiresEventLoopForChannel: true)
let mockRequestWithRequiredEventLoop = MockHTTPScheduableRequest(
eventLoop: el2,
requiresEventLoopForChannel: true
)
let requestWithRequiredEventLoop = HTTPConnectionPool.Request(mockRequestWithRequiredEventLoop)
let action2 = state.executeRequest(requestWithRequiredEventLoop)
guard case .createConnection(let http1ConnId, let http1EventLoop) = action2.connection else {
return XCTFail("Unexpected connection action \(action2.connection)")
}
XCTAssertTrue(http1EventLoop === el2)
XCTAssertEqual(action2.request, .scheduleRequestTimeout(for: requestWithRequiredEventLoop, on: mockRequestWithRequiredEventLoop.eventLoop))
XCTAssertEqual(
action2.request,
.scheduleRequestTimeout(for: requestWithRequiredEventLoop, on: mockRequestWithRequiredEventLoop.eventLoop)
)
XCTAssertNoThrow(try connections.createConnection(http1ConnId, on: el2))
XCTAssertNoThrow(try queuer.queue(mockRequestWithRequiredEventLoop, id: requestWithRequiredEventLoop.id))
@@ -1048,7 +1144,10 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
guard case .executeRequest(let request2, http1Conn, cancelTimeout: true) = migrationAction2.request else {
return XCTFail("unexpected request action \(migrationAction2.request)")
}
guard case .migration(let createConnections, closeConnections: [], scheduleTimeout: nil) = migrationAction2.connection else {
guard
case .migration(let createConnections, closeConnections: [], scheduleTimeout: nil) = migrationAction2
.connection
else {
return XCTFail("unexpected connection action \(migrationAction2.connection)")
}
XCTAssertEqual(createConnections.map { $0.1.id }, [el2.id])
@@ -1102,14 +1201,20 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
}
// a request with new required event loop should create a new connection
let mockRequestWithRequiredEventLoop = MockHTTPScheduableRequest(eventLoop: el2, requiresEventLoopForChannel: true)
let mockRequestWithRequiredEventLoop = MockHTTPScheduableRequest(
eventLoop: el2,
requiresEventLoopForChannel: true
)
let requestWithRequiredEventLoop = HTTPConnectionPool.Request(mockRequestWithRequiredEventLoop)
let action2 = state.executeRequest(requestWithRequiredEventLoop)
guard case .createConnection(let http1ConnId, let http1EventLoop) = action2.connection else {
return XCTFail("Unexpected connection action \(action2.connection)")
}
XCTAssertTrue(http1EventLoop === el2)
XCTAssertEqual(action2.request, .scheduleRequestTimeout(for: requestWithRequiredEventLoop, on: mockRequestWithRequiredEventLoop.eventLoop))
XCTAssertEqual(
action2.request,
.scheduleRequestTimeout(for: requestWithRequiredEventLoop, on: mockRequestWithRequiredEventLoop.eventLoop)
)
XCTAssertNoThrow(try connections.createConnection(http1ConnId, on: el2))
XCTAssertNoThrow(try queuer.queue(mockRequestWithRequiredEventLoop, id: requestWithRequiredEventLoop.id))
@@ -1131,7 +1236,10 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
XCTAssertNoThrow(try connections.succeedConnectionCreationHTTP1(http1ConnId))
let migrationAction2 = state.newHTTP1ConnectionCreated(http1Conn)
XCTAssertEqual(migrationAction2.request, .none)
XCTAssertEqual(migrationAction2.connection, .migration(createConnections: [], closeConnections: [http1Conn], scheduleTimeout: nil))
XCTAssertEqual(
migrationAction2.connection,
.migration(createConnections: [], closeConnections: [http1Conn], scheduleTimeout: nil)
)
// in http/1 state, we should close idle http2 connections
XCTAssertNoThrow(try connections.finishExecution(http2Conn.id))
@@ -1234,10 +1342,16 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
case 0:
XCTAssertEqual(executeAction.connection, .cancelTimeoutTimer(generalPurposeConnection.id))
XCTAssertNoThrow(try connections.activateConnection(generalPurposeConnection.id))
XCTAssertEqual(executeAction.request, .executeRequest(request, generalPurposeConnection, cancelTimeout: false))
XCTAssertEqual(
executeAction.request,
.executeRequest(request, generalPurposeConnection, cancelTimeout: false)
)
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
case 1..<100:
XCTAssertEqual(executeAction.request, .executeRequest(request, generalPurposeConnection, cancelTimeout: false))
XCTAssertEqual(
executeAction.request,
.executeRequest(request, generalPurposeConnection, cancelTimeout: false)
)
XCTAssertEqual(executeAction.connection, .none)
XCTAssertNoThrow(try connections.execute(mockRequest, on: generalPurposeConnection))
case 100..<1000:
@@ -1255,7 +1369,8 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
XCTAssertEqual(finishAction.connection, .none)
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request else {
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request
else {
return XCTFail("Unexpected request action: \(finishAction.request)")
}
guard requests.count == 1, let request = requests.first else {
@@ -1270,11 +1385,23 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// Next the server allows for more concurrent streams
let newMaxStreams = 200
XCTAssertNoThrow(try connections.newHTTP2ConnectionSettingsReceived(generalPurposeConnection.id, maxConcurrentStreams: newMaxStreams))
let newMaxStreamsAction = state.newHTTP2MaxConcurrentStreamsReceived(generalPurposeConnection.id, newMaxStreams: newMaxStreams)
XCTAssertNoThrow(
try connections.newHTTP2ConnectionSettingsReceived(
generalPurposeConnection.id,
maxConcurrentStreams: newMaxStreams
)
)
let newMaxStreamsAction = state.newHTTP2MaxConcurrentStreamsReceived(
generalPurposeConnection.id,
newMaxStreams: newMaxStreams
)
XCTAssertEqual(newMaxStreamsAction.connection, .none)
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = newMaxStreamsAction.request else {
return XCTFail("Unexpected request action after new max concurrent stream setting: \(newMaxStreamsAction.request)")
guard
case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = newMaxStreamsAction.request
else {
return XCTFail(
"Unexpected request action after new max concurrent stream setting: \(newMaxStreamsAction.request)"
)
}
XCTAssertEqual(requests.count, 100, "Expected to execute 100 more requests")
for request in requests {
@@ -1291,7 +1418,8 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
XCTAssertEqual(finishAction.connection, .none)
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request else {
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request
else {
return XCTFail("Unexpected request action: \(finishAction.request)")
}
guard requests.count == 1, let request = requests.first else {
@@ -1304,8 +1432,16 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
// Next the server allows for fewer concurrent streams
let fewerMaxStreams = 50
XCTAssertNoThrow(try connections.newHTTP2ConnectionSettingsReceived(generalPurposeConnection.id, maxConcurrentStreams: fewerMaxStreams))
let fewerMaxStreamsAction = state.newHTTP2MaxConcurrentStreamsReceived(generalPurposeConnection.id, newMaxStreams: fewerMaxStreams)
XCTAssertNoThrow(
try connections.newHTTP2ConnectionSettingsReceived(
generalPurposeConnection.id,
maxConcurrentStreams: fewerMaxStreams
)
)
let fewerMaxStreamsAction = state.newHTTP2MaxConcurrentStreamsReceived(
generalPurposeConnection.id,
newMaxStreams: fewerMaxStreams
)
XCTAssertEqual(fewerMaxStreamsAction.connection, .none)
XCTAssertEqual(fewerMaxStreamsAction.request, .none)
@@ -1323,7 +1459,8 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
XCTAssertNoThrow(try connections.finishExecution(generalPurposeConnection.id))
let finishAction = state.http2ConnectionStreamClosed(generalPurposeConnection.id)
XCTAssertEqual(finishAction.connection, .none)
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request else {
guard case .executeRequestsAndCancelTimeouts(let requests, generalPurposeConnection) = finishAction.request
else {
return XCTFail("Unexpected request action: \(finishAction.request)")
}
guard requests.count == 1, let request = requests.first else {
@@ -1343,7 +1480,10 @@ class HTTPConnectionPool_HTTP2StateMachineTests: XCTestCase {
switch remaining {
case 1:
timeoutTimerScheduled = true
XCTAssertEqual(finishAction.connection, .scheduleTimeoutTimer(generalPurposeConnection.id, on: generalPurposeConnection.eventLoop))
XCTAssertEqual(
finishAction.connection,
.scheduleTimeoutTimer(generalPurposeConnection.id, on: generalPurposeConnection.eventLoop)
)
XCTAssertNoThrow(try connections.parkConnection(generalPurposeConnection.id))
case 2...50:
XCTAssertEqual(finishAction.connection, .none)
@@ -1388,13 +1528,17 @@ func XCTAssertEqualTypeAndValue<Left, Right: Equatable>(
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertNoThrow(try {
let lhs = try lhs()
let rhs = try rhs()
guard let lhsAsRhs = lhs as? Right else {
XCTFail("could not cast \(lhs) of type \(type(of: lhs)) to \(type(of: rhs))", file: file, line: line)
return
}
XCTAssertEqual(lhsAsRhs, rhs, file: file, line: line)
}(), file: file, line: line)
XCTAssertNoThrow(
try {
let lhs = try lhs()
let rhs = try rhs()
guard let lhsAsRhs = lhs as? Right else {
XCTFail("could not cast \(lhs) of type \(type(of: lhs)) to \(type(of: rhs))", file: file, line: line)
return
}
XCTAssertEqual(lhsAsRhs, rhs, file: file, line: line)
}(),
file: file,
line: line
)
}
@@ -12,12 +12,13 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOHTTP1
import NIOPosix
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPool_ManagerTests: XCTestCase {
func testManagerHappyPath() {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 4)
@@ -49,15 +50,17 @@ class HTTPConnectionPool_ManagerTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -105,15 +108,17 @@ class HTTPConnectionPool_ManagerTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -12,7 +12,6 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOEmbedded
@@ -20,6 +19,8 @@ import NIOHTTP1
import NIOSSL
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPool_RequestQueueTests: XCTestCase {
func testCountAndIsEmptyWorks() {
var queue = HTTPConnectionPool.RequestQueue()
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Atomics
import Dispatch
import NIOConcurrencyHelpers
import NIOCore
import NIOEmbedded
@testable import AsyncHTTPClient
/// An `EventLoopGroup` of `EmbeddedEventLoop`s.
final class EmbeddedEventLoopGroup: EventLoopGroup {
private let loops: [EmbeddedEventLoop]
@@ -34,7 +35,7 @@ final class EmbeddedEventLoopGroup: EventLoopGroup {
}
internal func makeIterator() -> EventLoopIterator {
return EventLoopIterator(self.loops)
EventLoopIterator(self.loops)
}
internal func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
@@ -56,7 +57,7 @@ final class EmbeddedEventLoopGroup: EventLoopGroup {
extension HTTPConnectionPool.Request: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
lhs.id == rhs.id
}
}
@@ -78,15 +79,24 @@ extension HTTPConnectionPool.StateMachine.ConnectionAction: Equatable {
switch (lhs, rhs) {
case (.createConnection(let lhsConnID, on: let lhsEL), .createConnection(let rhsConnID, on: let rhsEL)):
return lhsConnID == rhsConnID && lhsEL === rhsEL
case (.scheduleBackoffTimer(let lhsConnID, let lhsBackoff, on: let lhsEL), .scheduleBackoffTimer(let rhsConnID, let rhsBackoff, on: let rhsEL)):
case (
.scheduleBackoffTimer(let lhsConnID, let lhsBackoff, on: let lhsEL),
.scheduleBackoffTimer(let rhsConnID, let rhsBackoff, on: let rhsEL)
):
return lhsConnID == rhsConnID && lhsBackoff == rhsBackoff && lhsEL === rhsEL
case (.scheduleTimeoutTimer(let lhsConnID, on: let lhsEL), .scheduleTimeoutTimer(let rhsConnID, on: let rhsEL)):
return lhsConnID == rhsConnID && lhsEL === rhsEL
case (.cancelTimeoutTimer(let lhsConnID), .cancelTimeoutTimer(let rhsConnID)):
return lhsConnID == rhsConnID
case (.closeConnection(let lhsConn, isShutdown: let lhsShut), .closeConnection(let rhsConn, isShutdown: let rhsShut)):
case (
.closeConnection(let lhsConn, isShutdown: let lhsShut),
.closeConnection(let rhsConn, isShutdown: let rhsShut)
):
return lhsConn == rhsConn && lhsShut == rhsShut
case (.cleanupConnections(let lhsContext, isShutdown: let lhsShut), .cleanupConnections(let rhsContext, isShutdown: let rhsShut)):
case (
.cleanupConnections(let lhsContext, isShutdown: let lhsShut),
.cleanupConnections(let rhsContext, isShutdown: let rhsShut)
):
return lhsContext == rhsContext && lhsShut == rhsShut
case (
.migration(
@@ -100,12 +110,13 @@ extension HTTPConnectionPool.StateMachine.ConnectionAction: Equatable {
let rhsScheduleTimeout
)
):
return lhsCreateConnections.elementsEqual(rhsCreateConnections, by: {
$0.0 == $1.0 && $0.1 === $1.1
}) &&
lhsCloseConnections == rhsCloseConnections &&
lhsScheduleTimeout?.0 == rhsScheduleTimeout?.0 &&
lhsScheduleTimeout?.1 === rhsScheduleTimeout?.1
return lhsCreateConnections.elementsEqual(
rhsCreateConnections,
by: {
$0.0 == $1.0 && $0.1 === $1.1
}
) && lhsCloseConnections == rhsCloseConnections && lhsScheduleTimeout?.0 == rhsScheduleTimeout?.0
&& lhsScheduleTimeout?.1 === rhsScheduleTimeout?.1
case (.none, .none):
return true
default:
@@ -117,15 +128,27 @@ extension HTTPConnectionPool.StateMachine.ConnectionAction: Equatable {
extension HTTPConnectionPool.StateMachine.RequestAction: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.executeRequest(let lhsReq, let lhsConn, let lhsReqID), .executeRequest(let rhsReq, let rhsConn, let rhsReqID)):
case (
.executeRequest(let lhsReq, let lhsConn, let lhsReqID),
.executeRequest(let rhsReq, let rhsConn, let rhsReqID)
):
return lhsReq == rhsReq && lhsConn == rhsConn && lhsReqID == rhsReqID
case (.executeRequestsAndCancelTimeouts(let lhsReqs, let lhsConn), .executeRequestsAndCancelTimeouts(let rhsReqs, let rhsConn)):
case (
.executeRequestsAndCancelTimeouts(let lhsReqs, let lhsConn),
.executeRequestsAndCancelTimeouts(let rhsReqs, let rhsConn)
):
return lhsReqs.elementsEqual(rhsReqs, by: { $0 == $1 }) && lhsConn == rhsConn
case (.failRequest(let lhsReq, _, cancelTimeout: let lhsReqID), .failRequest(let rhsReq, _, cancelTimeout: let rhsReqID)):
case (
.failRequest(let lhsReq, _, cancelTimeout: let lhsReqID),
.failRequest(let rhsReq, _, cancelTimeout: let rhsReqID)
):
return lhsReq == rhsReq && lhsReqID == rhsReqID
case (.failRequestsAndCancelTimeouts(let lhsReqs, _), .failRequestsAndCancelTimeouts(let rhsReqs, _)):
return lhsReqs.elementsEqual(rhsReqs, by: { $0 == $1 })
case (.scheduleRequestTimeout(for: let lhsReq, on: let lhsEL), .scheduleRequestTimeout(for: let rhsReq, on: let rhsEL)):
case (
.scheduleRequestTimeout(for: let lhsReq, on: let lhsEL),
.scheduleRequestTimeout(for: let rhsReq, on: let rhsEL)
):
return lhsReq == rhsReq && lhsEL === rhsEL
case (.none, .none):
return true
@@ -146,7 +169,10 @@ extension HTTPConnectionPool.HTTP2StateMachine.EstablishedConnectionAction: Equa
switch (lhs, rhs) {
case (.scheduleTimeoutTimer(let lhsConnID, on: let lhsEL), .scheduleTimeoutTimer(let rhsConnID, on: let rhsEL)):
return lhsConnID == rhsConnID && lhsEL === rhsEL
case (.closeConnection(let lhsConn, isShutdown: let lhsShut), .closeConnection(let rhsConn, isShutdown: let rhsShut)):
case (
.closeConnection(let lhsConn, isShutdown: let lhsShut),
.closeConnection(let rhsConn, isShutdown: let rhsShut)
):
return lhsConn == rhsConn && lhsShut == rhsShut
case (.none, .none):
return true
@@ -12,13 +12,14 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOHTTP1
import NIOPosix
import XCTest
@testable import AsyncHTTPClient
class HTTPConnectionPoolTests: XCTestCase {
func testOnlyOneConnectionIsUsedForSubSequentRequests() {
let httpBin = HTTPBin()
@@ -53,15 +54,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -111,15 +114,19 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .init(.testOnly_exact(channelOn: eventLoopGroup.next(), delegateOn: eventLoopGroup.next())),
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .init(
.testOnly_exact(channelOn: eventLoopGroup.next(), delegateOn: eventLoopGroup.next())
),
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -170,15 +177,19 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .init(.testOnly_exact(channelOn: eventLoopGroup.next(), delegateOn: eventLoopGroup.next())),
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .init(
.testOnly_exact(channelOn: eventLoopGroup.next(), delegateOn: eventLoopGroup.next())
),
task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -225,15 +236,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .distantFuture,
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -279,15 +292,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -327,15 +342,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -383,15 +400,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -429,15 +448,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequest: HTTPClient.Request?
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "http://localhost:\(httpBin.port)/wait"))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: .init(label: "test")),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
@@ -489,15 +510,17 @@ class HTTPConnectionPoolTests: XCTestCase {
var maybeRequestBag: RequestBag<ResponseAccumulator>?
XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: url))
XCTAssertNoThrow(maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
))
XCTAssertNoThrow(
maybeRequestBag = try RequestBag(
request: XCTUnwrap(maybeRequest),
eventLoopPreference: .indifferent,
task: .init(eventLoop: eventLoopGroup.next(), logger: logger),
redirectHandler: nil,
connectionDeadline: .now() + .seconds(5),
requestOptions: .forTests(),
delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
)
)
guard let requestBag = maybeRequestBag else { return XCTFail("Expected to get a request") }
pool.executeRequest(requestBag)
@@ -521,7 +544,10 @@ class HTTPConnectionPoolTests: XCTestCase {
var backoff = HTTPConnectionPool.calculateBackoff(failedAttempt: 1)
// The value should be 100ms±3ms
XCTAssertLessThanOrEqual((backoff - .milliseconds(100)).nanoseconds.magnitude, TimeAmount.milliseconds(3).nanoseconds.magnitude)
XCTAssertLessThanOrEqual(
(backoff - .milliseconds(100)).nanoseconds.magnitude,
TimeAmount.milliseconds(3).nanoseconds.magnitude
)
// Should always increase
// We stop when we get within the jitter of 60s, which is 1.8s
@@ -537,7 +563,8 @@ class HTTPConnectionPoolTests: XCTestCase {
// Ok, now we should be able to do a hundred increments, and always hit 60s, plus or minus 1.8s of jitter.
for offset in 0..<100 {
XCTAssertLessThanOrEqual(
(HTTPConnectionPool.calculateBackoff(failedAttempt: attempt + offset) - .seconds(60)).nanoseconds.magnitude,
(HTTPConnectionPool.calculateBackoff(failedAttempt: attempt + offset) - .seconds(60)).nanoseconds
.magnitude,
TimeAmount.milliseconds(1800).nanoseconds.magnitude
)
}
@@ -12,22 +12,29 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOSSL
import XCTest
@testable import AsyncHTTPClient
class HTTPRequestStateMachineTests: XCTestCase {
func testSimpleGETRequest() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init([responseBody])))
@@ -36,10 +43,21 @@ class HTTPRequestStateMachineTests: XCTestCase {
func testPOSTRequestWithWriterBackpressure() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "4")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "4")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0]))
let part1 = IOData.byteBuffer(ByteBuffer(bytes: [1]))
let part2 = IOData.byteBuffer(ByteBuffer(bytes: [2]))
@@ -62,7 +80,10 @@ class HTTPRequestStateMachineTests: XCTestCase {
XCTAssertEqual(state.requestStreamFinished(promise: nil), .sendRequestEnd(nil))
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init([responseBody])))
@@ -71,14 +92,25 @@ class HTTPRequestStateMachineTests: XCTestCase {
func testPOSTContentLengthIsTooLong() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "4")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "4")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3]))
let part1 = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3]))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
state.requestStreamPartReceived(part1, promise: nil).assertFailRequest(HTTPClientError.bodyLengthMismatch, .close(nil))
state.requestStreamPartReceived(part1, promise: nil).assertFailRequest(
HTTPClientError.bodyLengthMismatch,
.close(nil)
)
// if another error happens the new one is ignored
XCTAssertEqual(state.errorHappened(HTTPClientError.remoteConnectionClosed), .wait)
@@ -86,9 +118,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
func testPOSTContentLengthIsTooShort() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "8")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "8")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(8))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3]))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
@@ -97,28 +137,51 @@ class HTTPRequestStateMachineTests: XCTestCase {
func testRequestBodyStreamIsCancelledIfServerRespondsWith301() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "12")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false)
)
let part = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3]))
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .sendBodyPart(part, nil))
// response is coming before having send all data
let responseHead = HTTPResponseHead(version: .http1_1, status: .movedPermanently)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: true))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: true)
)
XCTAssertEqual(state.writabilityChanged(writable: false), .wait)
XCTAssertEqual(state.writabilityChanged(writable: true), .wait)
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamPartReceived(part, promise: nil),
.failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, .init()))
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamPartReceived(part, promise: nil),
.failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
XCTAssertEqual(state.requestStreamFinished(promise: nil), .failSendStreamFinished(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamFinished(promise: nil),
.failSendStreamFinished(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
}
func testStreamPartReceived_whenCancelled() {
@@ -126,47 +189,84 @@ class HTTPRequestStateMachineTests: XCTestCase {
let part = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3]))
XCTAssertEqual(state.requestCancelled(), .failRequest(HTTPClientError.cancelled, .none))
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .failSendBodyPart(HTTPClientError.cancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamPartReceived(part, promise: nil),
.failSendBodyPart(HTTPClientError.cancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
}
func testRequestBodyStreamIsCancelledIfServerRespondsWith301WhileWriteBackpressure() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "12")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(state.headSent(), .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
XCTAssertEqual(
state.headSent(),
.notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false)
)
let part = IOData.byteBuffer(ByteBuffer(bytes: [0, 1, 2, 3]))
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .sendBodyPart(part, nil))
XCTAssertEqual(state.writabilityChanged(writable: false), .pauseRequestBodyStream)
// response is coming before having send all data
let responseHead = HTTPResponseHead(version: .http1_1, status: .movedPermanently)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.writabilityChanged(writable: true), .wait)
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamPartReceived(part, promise: nil),
.failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.close, .init()))
XCTAssertEqual(state.requestStreamPartReceived(part, promise: nil), .failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamPartReceived(part, promise: nil),
.failSendBodyPart(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
XCTAssertEqual(state.requestStreamFinished(promise: nil), .failSendStreamFinished(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300")
XCTAssertEqual(
state.requestStreamFinished(promise: nil),
.failSendStreamFinished(HTTPClientError.requestStreamCancelled, nil),
"Expected to drop all stream data after having received a response head, with status >= 300"
)
}
func testRequestBodyStreamIsContinuedIfServerRespondsWith200() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "12")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: 0...3))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
// response is coming before having send all data
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.end(nil)), .forwardResponseBodyParts(.init()))
let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7))
@@ -175,20 +275,34 @@ class HTTPRequestStateMachineTests: XCTestCase {
XCTAssertEqual(state.requestStreamPartReceived(part2, promise: nil), .sendBodyPart(part2, nil))
XCTAssertEqual(state.requestStreamFinished(promise: nil), .succeedRequest(.sendRequestEnd(nil), .init()))
XCTAssertEqual(state.requestStreamPartReceived(part2, promise: nil), .failSendBodyPart(HTTPClientError.requestStreamCancelled, nil))
XCTAssertEqual(
state.requestStreamPartReceived(part2, promise: nil),
.failSendBodyPart(HTTPClientError.requestStreamCancelled, nil)
)
}
func testRequestBodyStreamIsContinuedIfServerSendHeadWithStatus200() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "12")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: 0...3))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
// response is coming before having send all data
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7))
XCTAssertEqual(state.requestStreamPartReceived(part1, promise: nil), .sendBodyPart(part1, nil))
@@ -201,15 +315,26 @@ class HTTPRequestStateMachineTests: XCTestCase {
func testRequestIsFailedIfRequestBodySizeIsWrongEvenAfterServerRespondedWith200() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "12")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: 0...3))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
// response is coming before having send all data
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.end(nil)), .forwardResponseBodyParts(.init()))
let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7))
@@ -220,15 +345,26 @@ class HTTPRequestStateMachineTests: XCTestCase {
func testRequestIsFailedIfRequestBodySizeIsWrongEvenAfterServerSendHeadWithStatus200() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: HTTPHeaders([("content-length", "12")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .POST,
uri: "/",
headers: HTTPHeaders([("content-length", "12")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(12))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part0 = IOData.byteBuffer(ByteBuffer(bytes: 0...3))
XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil))
// response is coming before having send all data
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part1 = IOData.byteBuffer(ByteBuffer(bytes: 4...7))
XCTAssertEqual(state.requestStreamPartReceived(part1, promise: nil), .sendBodyPart(part1, nil))
@@ -245,7 +381,10 @@ class HTTPRequestStateMachineTests: XCTestCase {
XCTAssertEqual(state.writabilityChanged(writable: true), .sendRequestHead(requestHead, sendEnd: true))
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init([responseBody])))
@@ -264,10 +403,20 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part0 = ByteBuffer(bytes: 0...3)
let part1 = ByteBuffer(bytes: 4...7)
let part2 = ByteBuffer(bytes: 8...11)
@@ -291,10 +440,20 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part0 = ByteBuffer(bytes: 0...3)
let part1 = ByteBuffer(bytes: 4...7)
let part2 = ByteBuffer(bytes: 8...11)
@@ -318,10 +477,20 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part0 = ByteBuffer(bytes: 0...3)
let part1 = ByteBuffer(bytes: 4...7)
let part2 = ByteBuffer(bytes: 8...11)
@@ -336,7 +505,11 @@ class HTTPRequestStateMachineTests: XCTestCase {
XCTAssertEqual(state.read(), .read)
XCTAssertEqual(state.channelRead(.body(part2)), .wait)
XCTAssertEqual(state.read(), .read, "Calling `read` while we wait for a channelReadComplete doesn't crash")
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait, "Calling `demandMoreResponseBodyParts` while we wait for a channelReadComplete doesn't crash")
XCTAssertEqual(
state.demandMoreResponseBodyParts(),
.wait,
"Calling `demandMoreResponseBodyParts` while we wait for a channelReadComplete doesn't crash"
)
XCTAssertEqual(state.channelReadComplete(), .forwardResponseBodyParts(.init([part2])))
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -365,11 +538,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
// --- sending request
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
// --- receiving response
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["content-length": "4"])
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let responseBody = ByteBuffer(bytes: [1, 2, 3, 4])
XCTAssertEqual(state.channelRead(.body(responseBody)), .wait)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init([responseBody])))
@@ -380,27 +559,51 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
state.requestCancelled().assertFailRequest(HTTPClientError.cancelled, .close(nil))
}
func testRemoteSuddenlyClosesTheConnection() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: .init([("content-length", "4")]))
let requestHead = HTTPRequestHead(
version: .http1_1,
method: .GET,
uri: "/",
headers: .init([("content-length", "4")])
)
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(4))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
state.requestCancelled().assertFailRequest(HTTPClientError.cancelled, .close(nil))
XCTAssertEqual(state.requestStreamPartReceived(.byteBuffer(.init(bytes: 1...3)), promise: nil), .failSendBodyPart(HTTPClientError.cancelled, nil))
XCTAssertEqual(
state.requestStreamPartReceived(.byteBuffer(.init(bytes: 1...3)), promise: nil),
.failSendBodyPart(HTTPClientError.cancelled, nil)
)
}
func testReadTimeoutLeadsToFailureWithEverythingAfterBeingIgnored() {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: HTTPHeaders([("content-length", "12")]))
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
let responseHead = HTTPResponseHead(
version: .http1_1,
status: .ok,
headers: HTTPHeaders([("content-length", "12")])
)
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
let part0 = ByteBuffer(bytes: 0...3)
XCTAssertEqual(state.channelRead(.body(part0)), .wait)
state.idleReadTimeoutTriggered().assertFailRequest(HTTPClientError.readTimeout, .close(nil))
@@ -414,13 +617,19 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let continueHead = HTTPResponseHead(version: .http1_1, status: .continue)
XCTAssertEqual(state.channelRead(.head(continueHead)), .wait)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init()))
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -430,10 +639,16 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init()))
XCTAssertEqual(state.idleReadTimeoutTriggered(), .wait, "A read timeout that fires to late must be ignored")
}
@@ -442,10 +657,16 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.channelRead(.end(nil)), .succeedRequest(.none, .init()))
XCTAssertEqual(state.requestCancelled(), .wait, "A cancellation that happens to late is ignored")
}
@@ -454,9 +675,15 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
state.errorHappened(HTTPParserError.invalidChunkSize).assertFailRequest(HTTPParserError.invalidChunkSize, .close(nil))
state.errorHappened(HTTPParserError.invalidChunkSize).assertFailRequest(
HTTPParserError.invalidChunkSize,
.close(nil)
)
XCTAssertEqual(state.requestCancelled(), .wait, "A cancellation that happens to late is ignored")
}
@@ -464,10 +691,16 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_0, status: .internalServerError)
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -480,11 +713,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_0, status: .internalServerError)
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -498,13 +737,22 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .stream)
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: false))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: false)
)
let part1: ByteBuffer = .init(string: "foo")
XCTAssertEqual(state.requestStreamPartReceived(.byteBuffer(part1), promise: nil), .sendBodyPart(.byteBuffer(part1), nil))
XCTAssertEqual(
state.requestStreamPartReceived(.byteBuffer(part1), promise: nil),
.sendBodyPart(.byteBuffer(part1), nil)
)
let responseHead = HTTPResponseHead(version: .http1_0, status: .ok)
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -518,11 +766,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok)
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelRead(.body(body)), .wait)
state.errorHappened(NIOSSLError.uncleanShutdown).assertFailRequest(NIOSSLError.uncleanShutdown, .close(nil))
@@ -534,7 +788,10 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait)
state.channelInactive().assertFailRequest(HTTPClientError.remoteConnectionClosed, .none)
@@ -545,7 +802,10 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
state.errorHappened(ArbitraryError()).assertFailRequest(ArbitraryError(), .close(nil))
XCTAssertEqual(state.channelInactive(), .wait)
@@ -555,17 +815,26 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["content-length": "30"])
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.read(), .read)
XCTAssertEqual(state.channelRead(.body(body)), .wait)
XCTAssertEqual(state.channelReadComplete(), .forwardResponseBodyParts([body]))
XCTAssertEqual(state.errorHappened(NIOSSLError.uncleanShutdown), .wait)
state.errorHappened(HTTPParserError.invalidEOFState).assertFailRequest(HTTPParserError.invalidEOFState, .close(nil))
state.errorHappened(HTTPParserError.invalidEOFState).assertFailRequest(
HTTPParserError.invalidEOFState,
.close(nil)
)
XCTAssertEqual(state.channelInactive(), .wait)
}
@@ -573,11 +842,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["Content-Length": "50"])
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -594,11 +869,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["Content-Length": "50"])
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -615,11 +896,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["Content-Length": "50"])
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -635,11 +922,17 @@ class HTTPRequestStateMachineTests: XCTestCase {
var state = HTTPRequestStateMachine(isChannelWritable: true)
let requestHead = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/")
let metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))
XCTAssertEqual(state.startRequest(head: requestHead, metadata: metadata), .sendRequestHead(requestHead, sendEnd: true))
XCTAssertEqual(
state.startRequest(head: requestHead, metadata: metadata),
.sendRequestHead(requestHead, sendEnd: true)
)
let responseHead = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["Content-Length": "50"])
let body = ByteBuffer(string: "foo bar")
XCTAssertEqual(state.channelRead(.head(responseHead)), .forwardResponseHead(responseHead, pauseRequestBodyStream: false))
XCTAssertEqual(
state.channelRead(.head(responseHead)),
.forwardResponseHead(responseHead, pauseRequestBodyStream: false)
)
XCTAssertEqual(state.demandMoreResponseBodyParts(), .wait)
XCTAssertEqual(state.channelReadComplete(), .wait)
XCTAssertEqual(state.read(), .read)
@@ -688,13 +981,19 @@ extension HTTPRequestStateMachine.Action: Equatable {
case (.resumeRequestBodyStream, .resumeRequestBodyStream):
return true
case (.forwardResponseHead(let lhsHead, let lhsPauseRequestBodyStream), .forwardResponseHead(let rhsHead, let rhsPauseRequestBodyStream)):
case (
.forwardResponseHead(let lhsHead, let lhsPauseRequestBodyStream),
.forwardResponseHead(let rhsHead, let rhsPauseRequestBodyStream)
):
return lhsHead == rhsHead && lhsPauseRequestBodyStream == rhsPauseRequestBodyStream
case (.forwardResponseBodyParts(let lhsData), .forwardResponseBodyParts(let rhsData)):
return lhsData == rhsData
case (.succeedRequest(let lhsFinalAction, let lhsFinalBuffer), .succeedRequest(let rhsFinalAction, let rhsFinalBuffer)):
case (
.succeedRequest(let lhsFinalAction, let lhsFinalBuffer),
.succeedRequest(let rhsFinalAction, let rhsFinalBuffer)
):
return lhsFinalAction == rhsFinalAction && lhsFinalBuffer == rhsFinalBuffer
case (.failRequest(_, let lhsFinalAction), .failRequest(_, let rhsFinalAction)):
@@ -706,10 +1005,16 @@ extension HTTPRequestStateMachine.Action: Equatable {
case (.wait, .wait):
return true
case (.failSendBodyPart(let lhsError as HTTPClientError, let lhsPromise), .failSendBodyPart(let rhsError as HTTPClientError, let rhsPromise)):
case (
.failSendBodyPart(let lhsError as HTTPClientError, let lhsPromise),
.failSendBodyPart(let rhsError as HTTPClientError, let rhsPromise)
):
return lhsError == rhsError && lhsPromise?.futureResult == rhsPromise?.futureResult
case (.failSendStreamFinished(let lhsError as HTTPClientError, let lhsPromise), .failSendStreamFinished(let rhsError as HTTPClientError, let rhsPromise)):
case (
.failSendStreamFinished(let lhsError as HTTPClientError, let lhsPromise),
.failSendStreamFinished(let rhsError as HTTPClientError, let rhsPromise)
):
return lhsError == rhsError && lhsPromise?.futureResult == rhsPromise?.futureResult
default:
@@ -719,7 +1024,10 @@ extension HTTPRequestStateMachine.Action: Equatable {
}
extension HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction: Equatable {
public static func == (lhs: HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction, rhs: HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction) -> Bool {
public static func == (
lhs: HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction,
rhs: HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction
) -> Bool {
switch (lhs, rhs) {
case (.close, close):
return true
@@ -737,7 +1045,10 @@ extension HTTPRequestStateMachine.Action.FinalSuccessfulRequestAction: Equatable
}
extension HTTPRequestStateMachine.Action.FinalFailedRequestAction: Equatable {
public static func == (lhs: HTTPRequestStateMachine.Action.FinalFailedRequestAction, rhs: HTTPRequestStateMachine.Action.FinalFailedRequestAction) -> Bool {
public static func == (
lhs: HTTPRequestStateMachine.Action.FinalFailedRequestAction,
rhs: HTTPRequestStateMachine.Action.FinalFailedRequestAction
) -> Bool {
switch (lhs, rhs) {
case (.close(let lhsPromise), close(let rhsPromise)):
return lhsPromise?.futureResult == rhsPromise?.futureResult
@@ -759,7 +1070,11 @@ extension HTTPRequestStateMachine.Action {
line: UInt = #line
) where Error: Swift.Error & Equatable {
guard case .failRequest(let actualError, let actualFinalStreamAction) = self else {
return XCTFail("expected .failRequest(\(expectedError), \(expectedFinalStreamAction)) but got \(self)", file: file, line: line)
return XCTFail(
"expected .failRequest(\(expectedError), \(expectedFinalStreamAction)) but got \(self)",
file: file,
line: line
)
}
if let actualError = actualError as? Error {
XCTAssertEqual(actualError, expectedError, file: file, line: line)
@@ -14,9 +14,6 @@
import AsyncHTTPClient
import Atomics
#if canImport(Network)
import Network
#endif
import Logging
import NIOConcurrencyHelpers
import NIOCore
@@ -29,6 +26,10 @@ import NIOTestUtils
import NIOTransportServices
import XCTest
#if canImport(Network)
import Network
#endif
final class TestIdleTimeoutNoReuse: XCTestCaseHTTPClientTestsBaseClass {
func testIdleTimeoutNoReuse() throws {
var req = try HTTPClient.Request(url: self.defaultHTTPBinURLPrefix + "get", method: .GET)
@@ -12,9 +12,10 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import XCTest
@testable import AsyncHTTPClient
class LRUCacheTests: XCTestCase {
func testBasicsWork() {
var cache = LRUCache<Int, Int>(capacity: 1)
@@ -12,12 +12,13 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOHTTP1
import NIOSSL
@testable import AsyncHTTPClient
/// A mock connection pool (not creating any actual connections) that is used to validate
/// connection actions returned by the `HTTPConnectionPool.StateMachine`.
struct MockConnectionPool {
@@ -554,7 +555,9 @@ extension MockConnectionPool {
let request = HTTPConnectionPool.Request(mockRequest)
let action = state.executeRequest(request)
guard case .scheduleRequestTimeout(request, on: let waitEL) = action.request, mockRequest.eventLoop === waitEL else {
guard case .scheduleRequestTimeout(request, on: let waitEL) = action.request,
mockRequest.eventLoop === waitEL
else {
throw SetupError.expectedRequestToBeAddedToQueue
}
@@ -621,7 +624,9 @@ extension MockConnectionPool {
let request = HTTPConnectionPool.Request(mockRequest)
let executeAction = state.executeRequest(request)
guard case .scheduleRequestTimeout(request, on: let waitEL) = executeAction.request, mockRequest.eventLoop === waitEL else {
guard case .scheduleRequestTimeout(request, on: let waitEL) = executeAction.request,
mockRequest.eventLoop === waitEL
else {
throw SetupError.expectedRequestToBeAddedToQueue
}
@@ -634,7 +639,10 @@ extension MockConnectionPool {
// 2. the connection becomes available
let newConnection = try connections.succeedConnectionCreationHTTP2(connectionID, maxConcurrentStreams: maxConcurrentStreams)
let newConnection = try connections.succeedConnectionCreationHTTP2(
connectionID,
maxConcurrentStreams: maxConcurrentStreams
)
let action = state.newHTTP2ConnectionCreated(newConnection, maxConcurrentStreams: maxConcurrentStreams)
guard case .executeRequestsAndCancelTimeouts([request], newConnection) = action.request else {
@@ -674,10 +682,12 @@ final class MockHTTPScheduableRequest: HTTPSchedulableRequest {
let preferredEventLoop: EventLoop
let requiredEventLoop: EventLoop?
init(eventLoop: EventLoop,
logger: Logger = Logger(label: "mock"),
connectionTimeout: TimeAmount = .seconds(60),
requiresEventLoopForChannel: Bool = false) {
init(
eventLoop: EventLoop,
logger: Logger = Logger(label: "mock"),
connectionTimeout: TimeAmount = .seconds(60),
requiresEventLoopForChannel: Bool = false
) {
self.logger = logger
self.connectionDeadline = .now() + connectionTimeout
@@ -692,7 +702,7 @@ final class MockHTTPScheduableRequest: HTTPSchedulableRequest {
}
var eventLoop: EventLoop {
return self.preferredEventLoop
self.preferredEventLoop
}
// MARK: HTTPSchedulableRequest
@@ -12,12 +12,13 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOHTTP1
import XCTest
@testable import AsyncHTTPClient
final class MockHTTPExecutableRequest: HTTPExecutableRequest {
enum Event {
/// ``Event`` without associated values
@@ -12,10 +12,11 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIOConcurrencyHelpers
import NIOCore
@testable import AsyncHTTPClient
// This is a MockRequestExecutor, that is synchronized on its EventLoop.
final class MockRequestExecutor {
enum Errors: Error {
@@ -47,7 +48,7 @@ final class MockRequestExecutor {
}
var requestBodyPartsCount: Int {
return self.blockingQueue.count
self.blockingQueue.count
}
let eventLoop: EventLoop
@@ -82,7 +83,8 @@ final class MockRequestExecutor {
request.requestHeadSent()
}
func receiveRequestBody(deadline: NIODeadline = .now() + .seconds(5), _ verify: (ByteBuffer) throws -> Void) throws {
func receiveRequestBody(deadline: NIODeadline = .now() + .seconds(5), _ verify: (ByteBuffer) throws -> Void) throws
{
enum ReceiveAction {
case value(RequestParts)
case future(EventLoopFuture<RequestParts>)
@@ -155,10 +157,11 @@ final class MockRequestExecutor {
func receiveResponseDemand(deadline: NIODeadline = .now() + .seconds(5)) throws {
let secondsUntilDeath = deadline - NIODeadline.now()
guard self.responseBodyDemandLock.lock(
whenValue: true,
timeoutSeconds: .init(secondsUntilDeath.nanoseconds / 1_000_000_000)
)
guard
self.responseBodyDemandLock.lock(
whenValue: true,
timeoutSeconds: .init(secondsUntilDeath.nanoseconds / 1_000_000_000)
)
else {
throw TimeoutError()
}
@@ -168,10 +171,11 @@ final class MockRequestExecutor {
func receiveCancellation(deadline: NIODeadline = .now() + .seconds(5)) throws {
let secondsUntilDeath = deadline - NIODeadline.now()
guard self.cancellationLock.lock(
whenValue: true,
timeoutSeconds: .init(secondsUntilDeath.nanoseconds / 1_000_000_000)
)
guard
self.cancellationLock.lock(
whenValue: true,
timeoutSeconds: .init(secondsUntilDeath.nanoseconds / 1_000_000_000)
)
else {
throw TimeoutError()
}
@@ -265,8 +269,12 @@ extension MockRequestExecutor {
internal func popFirst(deadline: NIODeadline) throws -> Element {
let secondsUntilDeath = deadline - NIODeadline.now()
guard self.condition.lock(whenValue: true,
timeoutSeconds: .init(secondsUntilDeath.nanoseconds / 1_000_000_000)) else {
guard
self.condition.lock(
whenValue: true,
timeoutSeconds: .init(secondsUntilDeath.nanoseconds / 1_000_000_000)
)
else {
throw TimeoutError()
}
let first = self.buffer.removeFirst()
@@ -12,11 +12,12 @@
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import Logging
import NIOCore
import NIOHTTP1
@testable import AsyncHTTPClient
/// A mock request queue (not creating any timers) that is used to validate
/// request actions returned by the `HTTPConnectionPool.StateMachine`.
struct MockRequestQueuer {
@@ -47,9 +47,14 @@ class NWWaitingHandlerTests: XCTestCase {
let waitingEventHandler = NWWaitingHandler(requester: requester, connectionID: connectionID)
let embedded = EmbeddedChannel(handlers: [waitingEventHandler])
embedded.pipeline.fireUserInboundEventTriggered(NIOTSNetworkEvents.WaitingForConnectivity(transientError: .dns(1)))
embedded.pipeline.fireUserInboundEventTriggered(
NIOTSNetworkEvents.WaitingForConnectivity(transientError: .dns(1))
)
XCTAssertTrue(requester.waitingForConnectivityCalled, "Expected the handler to invoke .waitingForConnectivity on the requester")
XCTAssertTrue(
requester.waitingForConnectivityCalled,
"Expected the handler to invoke .waitingForConnectivity on the requester"
)
XCTAssertEqual(requester.connectionID, connectionID, "Expected the handler to pass connectionID to requester")
XCTAssertEqual(requester.transientError, NWError.dns(1))
}
@@ -60,7 +65,10 @@ class NWWaitingHandlerTests: XCTestCase {
let embedded = EmbeddedChannel(handlers: [waitingEventHandler])
embedded.pipeline.fireUserInboundEventTriggered(NIOTSNetworkEvents.BetterPathAvailable())
XCTAssertFalse(requester.waitingForConnectivityCalled, "Should not call .waitingForConnectivity on unrelated events")
XCTAssertFalse(
requester.waitingForConnectivityCalled,
"Should not call .waitingForConnectivity on unrelated events"
)
}
func testWaitingHandlerPassesTheEventDownTheContext() {

Some files were not shown because too many files have changed in this diff Show More