Files
async-http-client/Sources/AsyncHTTPClient/RequestBag+StateMachine.swift
T
e2ab0d176f Full support for bidirectional streaming (#879)
> ## Note: 
> This is a long LLM generated PR description. However it captures very
well, what has been changed and has already been reduced for brevity.
The PR is sadly quite complex but I think the description captures the
changes quite well.

This is foundational work needed to properly support HTTP trailers and
scenarios where the server sends a complete response before the client
finishes uploading (e.g., early rejection, 100-continue flows, or
bidirectional streaming protocols).

## Changes

### State Machine Improvements

- **Added `endForwarded` state** to
`Transaction.StateMachine.RequestStreamState`
- This new state distinguishes between "request data forwarded to the
channel" and "request data written to the network"
- Properly handles the race condition where response completes before
the request write completes

- **Renamed `succeedRequest` → `forwardResponseEnd`** in both
`HTTPRequestStateMachine.Action` and
`HTTP1ConnectionStateMachine.Action`
- Better reflects the semantic meaning: we're forwarding the end of the
response stream, not necessarily succeeding the entire request yet
  - More accurate naming for bidirectional streaming scenarios

### Protocol Changes

- **Added `requestBodyStreamSent()` to `HTTPExecutableRequest`
protocol**
- Called by the channel handler when the request body stream has been
fully written to the network
- Allows proper coordination between request and response stream
completion
  - Implemented in both `Transaction` and `RequestBag`

### Request State Machine Updates

- **Updated `FinalSuccessfulRequestAction`**
- Changed `.sendRequestEnd(EventLoopPromise<Void>?)` to simpler
`.requestDone`
- Added `.none` case for when response completes but request is still
in-flight
- Removed the need to pass promises around, simplifying the state
machine

- **`sendRequestEnd` action now includes
`FinalSuccessfulRequestAction`**
- Allows the state machine to signal what should happen after the
request completes
- Enables proper cleanup coordination (idle connection, close, or
continue)

### Channel Handler Updates

- **HTTP1ClientChannelHandler**
- `sendRequestEnd` now properly handles scenarios where response has
already completed
- Added future callback to coordinate request completion with final
actions
- Properly manages connection state (idle vs close) based on both
streams completing

- **HTTP2ClientRequestHandler**
  - Updated to handle new `sendRequestEnd` signature
  - Properly ignores HTTP/1-specific final actions (like `.requestDone`)

### RequestBag State Machine

- **Added `endReceived` state to `ResponseStreamState`**
- Tracks when the response has completed while request is still ongoing
- Enables proper sequencing: response end → request end → task
completion

- **Updated `FinishAction`**
- Added `.forwardStreamFinishedAndSucceedTask` for the case where both
streams complete simultaneously
  - Ensures delegate methods are called in the correct order

### Error Handling

- **Improved failure handling in `Transaction.StateMachine`**
- Now properly handles errors that occur after response completes but
before request finishes
  - Added `cancelExecutor` action to the fail path
- Executor is now passed to `failRequestStreamContinuation` for proper
cleanup

## Technical Details

### The Problem

Previously, when a server sent a complete response before the client
finished uploading the request body, AHC would:
1. Receive the full response (head, body, end)
2. But NOT inform the user that the response was complete if the request
was still streaming
3. Only succeed the request after both streams completed

This made it impossible to implement proper bidirectional streaming or
handle scenarios like:
- Server rejecting a large upload early (e.g., 413 Payload Too Large)
- 100-continue flows where the server responds before request completes
- HTTP trailers sent by the server

### The Solution

The new state machine properly tracks four completion states:
1. **Neither complete**: Normal request/response in flight
2. **Response complete, request ongoing**: New
`endForwarded`/`endReceived` states
3. **Request complete, response ongoing**: Existing logic
4. **Both complete**: Request succeeds

The key insight is the `endForwarded` state, which represents "we've
given all request data to the channel, but it hasn't been written to the
network yet". This allows us to:
- Immediately forward response completion to the user
- Wait for the write to complete before cleaning up resources
- Properly sequence connection state transitions

## Future Work

This PR lays the groundwork for:
- Proper internal HTTP trailer support (both sending and receiving)

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
2026-02-03 12:21:31 +01:00

703 lines
30 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOCore
import NIOHTTP1
import struct Foundation.URL
extension HTTPClient {
/// The maximum body size allowed, before a redirect response is cancelled. 3KB.
///
/// Why 3KB? We feel like this is a good compromise between potentially reusing the
/// connection in HTTP/1.1 mode (if we load all data from the redirect response we can
/// reuse the connection) and not being to wasteful in the amount of data that is thrown
/// away being transferred.
fileprivate static let maxBodySizeRedirectResponse = 1024 * 3
}
extension RequestBag {
struct StateMachine {
fileprivate enum State {
case initialized(RedirectHandler<Delegate.Response>?)
case queued(HTTPRequestScheduler, RedirectHandler<Delegate.Response>?)
/// if the deadline was exceeded while in the `.queued(_:)` state,
/// we wait until the request pool fails the request with a potential more descriptive error message,
/// if a connection failure has occured while the request was queued.
case deadlineExceededWhileQueued
case executing(HTTPRequestExecutor, RequestStreamState, ResponseStreamState)
case finished(error: Error?)
case redirected(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, Int, HTTPResponseHead, URL)
case modifying
}
fileprivate enum RequestStreamState {
case initialized
case producing
case paused(EventLoopPromise<Void>?)
case finished
}
fileprivate enum ResponseStreamState {
enum Next {
case askExecutorForMore
case error(Error)
case eof
}
case initialized(RedirectHandler<Delegate.Response>?)
case buffering(CircularBuffer<ByteBuffer>, next: Next)
case waitingForRemote
case endReceived
}
private var state: State
private let requestFramingMetadata: RequestFramingMetadata
init(redirectHandler: RedirectHandler<Delegate.Response>?, requestFramingMetadata: RequestFramingMetadata) {
self.state = .initialized(redirectHandler)
self.requestFramingMetadata = requestFramingMetadata
}
}
}
extension RequestBag.StateMachine {
mutating func requestWasQueued(_ scheduler: HTTPRequestScheduler) {
guard case .initialized(let redirectHandler) = self.state else {
// There might be a race between `requestWasQueued` and `willExecuteRequest`:
//
// If the request is created and passed to the HTTPClient on thread A, it will move into
// the connection pool lock in thread A. If no connection is available, thread A will
// add the request to the waiters and leave the connection pool lock.
// `requestWasQueued` will be called outside the connection pool lock on thread A.
// However if thread B has a connection that becomes available and thread B enters the
// connection pool lock directly after thread A, the request will be immediately
// scheduled for execution on thread B. After the thread B has left the lock it will
// call `willExecuteRequest` directly after.
//
// Having an order in the connection pool lock, does not guarantee an order in calling:
// `requestWasQueued` and `willExecuteRequest`.
//
// For this reason we must check the state here... If we are not `.initialized`, we are
// already executing.
return
}
self.state = .queued(scheduler, redirectHandler)
}
enum WillExecuteRequestAction {
case cancelExecuter(HTTPRequestExecutor)
case failTaskAndCancelExecutor(Error, HTTPRequestExecutor)
case none
}
mutating func requestHeadSent() {
switch self.state {
case .initialized:
fatalError()
case .executing(let executor, .initialized, let responseStream):
if self.requestFramingMetadata.body == .fixedSize(0) {
self.state = .executing(executor, .finished, responseStream)
}
default:
break
}
}
mutating func willExecuteRequest(_ executor: HTTPRequestExecutor) -> WillExecuteRequestAction {
switch self.state {
case .initialized(let redirectHandler), .queued(_, let redirectHandler):
self.state = .executing(executor, .initialized, .initialized(redirectHandler))
return .none
case .deadlineExceededWhileQueued:
let error: Error = HTTPClientError.deadlineExceeded
self.state = .finished(error: error)
return .failTaskAndCancelExecutor(error, executor)
case .finished(error: .some):
return .cancelExecuter(executor)
case .executing, .redirected, .finished(error: .none), .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum ResumeProducingAction {
case startWriter
case succeedBackpressurePromise(EventLoopPromise<Void>?)
case none
}
mutating func resumeRequestBodyStream() -> ResumeProducingAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("A request stream can only be resumed, if the request was started")
case .executing(let executor, .initialized, .initialized(let redirectHandler)):
self.state = .executing(executor, .producing, .initialized(redirectHandler))
return .startWriter
case .executing(_, .producing, _):
preconditionFailure("Expected that resume is only called when if we were paused before")
case .executing(let executor, .paused(let promise), let responseState):
self.state = .executing(executor, .producing, responseState)
return .succeedBackpressurePromise(promise)
case .executing(_, .finished, _):
// the channels writability changed to writable after we have forwarded all the
// request bytes. Can be ignored.
return .none
case .executing(_, .initialized, .buffering),
.executing(_, .initialized, .waitingForRemote),
.executing(_, .initialized, .endReceived):
preconditionFailure("Invalid states: Response can not be received before request")
case .redirected:
// if we are redirected, we should cancel our request body stream anyway
return .none
case .finished:
// If this task has been cancelled we may be in an error state. As a matter of
// defensive programming, we also tolerate receiving this notification if we've ended cleanly:
// while it shouldn't happen, nothing will go wrong if we just ignore it.
// All paths through this state machine should cancel our request body stream to get here anyway.
return .none
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
mutating func pauseRequestBodyStream() {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("A request stream can only be paused, if the request was started")
case .executing(let executor, let requestState, let responseState):
switch requestState {
case .initialized:
preconditionFailure("Request stream must be started before it can be paused")
case .producing:
self.state = .executing(executor, .paused(nil), responseState)
case .paused:
preconditionFailure("Expected that pause is only called when if we were producing before")
case .finished:
// the channels writability changed to not writable after we have forwarded the
// last bytes from our side.
break
}
case .redirected:
// if we are redirected, we should cancel our request body stream anyway
break
case .finished:
// the request is already finished nothing further to do
break
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum WriteAction {
case write(IOData, HTTPRequestExecutor, EventLoopFuture<Void>)
case failTask(Error)
case failFuture(Error)
}
mutating func writeNextRequestPart(_ part: IOData, taskEventLoop: EventLoop) -> WriteAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("Invalid state: \(self.state)")
case .executing(let executor, let requestState, let responseState):
switch requestState {
case .initialized:
preconditionFailure("Request stream must be started before it can be paused")
case .producing:
return .write(part, executor, taskEventLoop.makeSucceededFuture(()))
case .paused(.none):
// backpressure is signaled to the writer using unfulfilled futures. if there
// is no existing, unfulfilled promise, let's create a new one
let promise = taskEventLoop.makePromise(of: Void.self)
self.state = .executing(executor, .paused(promise), responseState)
return .write(part, executor, promise.futureResult)
case .paused(.some(let promise)):
// backpressure is signaled to the writer using unfulfilled futures. if an
// unfulfilled promise already exist, let's reuse the promise
return .write(part, executor, promise.futureResult)
case .finished:
let error = HTTPClientError.writeAfterRequestSent
self.state = .finished(error: error)
return .failTask(error)
}
case .redirected:
// if we are redirected we can cancel the upload stream
return .failFuture(HTTPClientError.cancelled)
case .finished(error: .some(let error)):
return .failFuture(error)
case .finished(error: .none):
return .failFuture(HTTPClientError.requestStreamCancelled)
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum FinishAction {
case forwardStreamFinished(HTTPRequestExecutor, EventLoopPromise<Void>?)
case forwardStreamFinishedAndSucceedTask(HTTPRequestExecutor, EventLoopPromise<Void>?)
case forwardStreamFailureAndFailTask(HTTPRequestExecutor, Error, EventLoopPromise<Void>?)
case none
}
mutating func finishRequestBodyStream(_ result: Result<Void, Error>) -> FinishAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("Invalid state: \(self.state)")
case .executing(let executor, let requestState, let responseState):
switch requestState {
case .initialized:
preconditionFailure("Request stream must be started before it can be finished")
case .producing:
switch result {
case .success:
switch responseState {
case .initialized, .buffering, .waitingForRemote:
self.state = .executing(executor, .finished, responseState)
return .forwardStreamFinished(executor, nil)
case .endReceived:
self.state = .finished(error: nil)
return .forwardStreamFinishedAndSucceedTask(executor, nil)
}
case .failure(let error):
self.state = .finished(error: error)
return .forwardStreamFailureAndFailTask(executor, error, nil)
}
case .paused(let promise):
switch result {
case .success:
switch responseState {
case .initialized, .buffering, .waitingForRemote:
self.state = .executing(executor, .finished, responseState)
return .forwardStreamFinished(executor, promise)
case .endReceived:
self.state = .finished(error: nil)
return .forwardStreamFinishedAndSucceedTask(executor, promise)
}
case .failure(let error):
self.state = .finished(error: error)
return .forwardStreamFailureAndFailTask(executor, error, promise)
}
case .finished:
preconditionFailure("How can a finished request stream, be finished again?")
}
case .redirected:
return .none
case .finished(error: _):
return .none
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum ReceiveResponseHeadAction {
case none
case forwardResponseHead(HTTPResponseHead)
case signalBodyDemand(HTTPRequestExecutor)
case redirect(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
}
/// The response head has been received.
///
/// - Parameter head: The response' head
/// - Returns: Whether the response should be forwarded to the delegate. Will be `false` if the request follows a redirect.
mutating func receiveResponseHead(_ head: HTTPResponseHead) -> ReceiveResponseHeadAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("How can we receive a response, if the request hasn't started yet.")
case .executing(let executor, let requestState, let responseState):
guard case .initialized(let redirectHandler) = responseState else {
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 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.
switch head.contentLength {
case .some(0...(HTTPClient.maxBodySizeRedirectResponse)), .none:
self.state = .redirected(executor, redirectHandler, 0, head, redirectURL)
return .signalBodyDemand(executor)
case .some:
self.state = .finished(error: HTTPClientError.cancelled)
return .redirect(executor, redirectHandler, head, redirectURL)
}
} else {
self.state = .executing(executor, requestState, .buffering(.init(), next: .askExecutorForMore))
return .forwardResponseHead(head)
}
case .redirected:
preconditionFailure("This state can only be reached after we have received a HTTP head")
case .finished(error: .some):
return .none
case .finished(error: .none):
preconditionFailure("How can the request be finished without error, before receiving response head?")
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum ReceiveResponseBodyAction {
case none
case forwardResponsePart(ByteBuffer)
case signalBodyDemand(HTTPRequestExecutor)
case redirect(HTTPRequestExecutor, RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
}
mutating func receiveResponseBodyParts(_ buffer: CircularBuffer<ByteBuffer>) -> ReceiveResponseBodyAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.")
case .executing(_, _, .initialized), .executing(_, _, .endReceived):
preconditionFailure("If we receive a response body, we must have received a head before")
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)"
)
}
self.state = .modifying
if currentBuffer.isEmpty {
currentBuffer = buffer
} else {
currentBuffer.append(contentsOf: buffer)
}
self.state = .executing(executor, requestState, .buffering(currentBuffer, next: next))
return .none
case .executing(let executor, let requestState, .waitingForRemote):
if buffer.count > 0 {
var buffer = buffer
let first = buffer.removeFirst()
self.state = .executing(executor, requestState, .buffering(buffer, next: .askExecutorForMore))
return .forwardResponsePart(first)
} else {
return .none
}
case .redirected(let executor, let redirectHandler, var receivedBytes, let head, let redirectURL):
let partsLength = buffer.reduce(into: 0) { $0 += $1.readableBytes }
receivedBytes += partsLength
if receivedBytes > HTTPClient.maxBodySizeRedirectResponse {
self.state = .finished(error: HTTPClientError.cancelled)
return .redirect(executor, redirectHandler, head, redirectURL)
} else {
self.state = .redirected(executor, redirectHandler, receivedBytes, head, redirectURL)
return .signalBodyDemand(executor)
}
case .finished(error: .some):
return .none
case .finished(error: .none):
preconditionFailure("How can the request be finished without error, before receiving response head?")
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum ReceiveResponseEndAction {
case consume(ByteBuffer)
case redirect(RedirectHandler<Delegate.Response>, HTTPResponseHead, URL)
case succeedRequest
case none
}
mutating func receiveResponseEnd(_ newChunks: CircularBuffer<ByteBuffer>?) -> ReceiveResponseEndAction {
switch self.state {
case .initialized, .queued, .deadlineExceededWhileQueued:
preconditionFailure("How can we receive a response body part, if the request hasn't started yet.")
case .executing(_, _, .initialized), .executing(_, _, .endReceived):
preconditionFailure("If we receive a response body, we must have received a head before")
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)"
)
}
if buffer.isEmpty, let newChunks = newChunks, !newChunks.isEmpty {
buffer = newChunks
} else if let newChunks = newChunks, !newChunks.isEmpty {
buffer.append(contentsOf: newChunks)
}
self.state = .executing(executor, requestState, .buffering(buffer, next: .eof))
return .none
case .executing(let executor, let requestState, .waitingForRemote):
guard var newChunks = newChunks, !newChunks.isEmpty else {
switch requestState {
case .initialized, .paused, .producing:
self.state = .executing(executor, requestState, .endReceived)
return .none
case .finished:
self.state = .finished(error: nil)
return .succeedRequest
}
}
let first = newChunks.removeFirst()
self.state = .executing(executor, requestState, .buffering(newChunks, next: .eof))
return .consume(first)
case .redirected(_, let redirectHandler, _, let head, let redirectURL):
self.state = .finished(error: nil)
return .redirect(redirectHandler, head, redirectURL)
case .finished(error: .some):
return .none
case .finished(error: .none):
preconditionFailure("How can the request be finished without error, before receiving response head?")
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
enum ConsumeAction {
case requestMoreFromExecutor(HTTPRequestExecutor)
case consume(ByteBuffer)
case finishStream
case failTask(Error, executorToCancel: HTTPRequestExecutor?)
case doNothing
}
mutating func consumeMoreBodyData(resultOfPreviousConsume result: Result<Void, Error>) -> ConsumeAction {
switch result {
case .success:
return self.consumeMoreBodyData()
case .failure(let error):
return self.failWithConsumptionError(error)
}
}
private mutating func failWithConsumptionError(_ error: Error) -> ConsumeAction {
switch self.state {
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"
)
case .executing(_, _, .buffering(_, next: .error(let connectionError))):
// if an error was received from the connection, we fail the task with the one
// from the connection, since it happened first.
self.state = .finished(error: connectionError)
return .failTask(connectionError, executorToCancel: nil)
case .executing(let executor, _, .buffering(_, _)):
self.state = .finished(error: error)
return .failTask(error, executorToCancel: executor)
case .executing(_, _, .waitingForRemote), .executing(_, _, .endReceived):
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."
)
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"
)
case .modifying:
preconditionFailure()
}
}
private mutating func consumeMoreBodyData() -> ConsumeAction {
switch self.state {
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"
)
case .executing(let executor, let requestState, .buffering(var buffer, next: .askExecutorForMore)):
self.state = .modifying
if let byteBuffer = buffer.popFirst() {
self.state = .executing(executor, requestState, .buffering(buffer, next: .askExecutorForMore))
return .consume(byteBuffer)
}
// buffer is empty, wait for more
self.state = .executing(executor, requestState, .waitingForRemote)
return .requestMoreFromExecutor(executor)
case .executing(let executor, let requestState, .buffering(var buffer, next: .eof)):
self.state = .modifying
if let byteBuffer = buffer.popFirst() {
self.state = .executing(executor, requestState, .buffering(buffer, next: .eof))
return .consume(byteBuffer)
}
self.state = .finished(error: nil)
return .finishStream
case .executing(_, _, .buffering(_, next: .error(let error))):
self.state = .finished(error: error)
return .failTask(error, executorToCancel: nil)
case .executing(_, _, .waitingForRemote):
preconditionFailure(
"Invalid state... We just returned from a consumption function. We can't already be waiting"
)
case .executing(_, _, .endReceived):
// we can't succeed the request here, as we have not sent all request parts.
return .doNothing
case .redirected:
return .doNothing
case .finished(error: .some):
return .doNothing
case .finished(error: .none):
preconditionFailure(
"Invalid state... If no error occurred, this must not be called, after the request was finished"
)
case .modifying:
preconditionFailure()
}
}
enum DeadlineExceededAction {
case cancelScheduler(HTTPRequestScheduler?)
case fail(FailAction)
}
mutating func deadlineExceeded() -> DeadlineExceededAction {
switch self.state {
case .queued(let queuer, _):
/// We do not fail the request immediately because we want to give the scheduler a chance of throwing a better error message
/// We therefore depend on the scheduler failing the request after we cancel the request.
self.state = .deadlineExceededWhileQueued
return .cancelScheduler(queuer)
case .initialized,
.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))
}
}
enum FailAction {
case failTask(Error, HTTPRequestScheduler?, HTTPRequestExecutor?)
case cancelExecutor(HTTPRequestExecutor)
case none
}
mutating func fail(_ error: Error) -> FailAction {
switch self.state {
case .initialized:
self.state = .finished(error: error)
return .failTask(error, nil, nil)
case .queued(let queuer, _):
self.state = .finished(error: error)
return .failTask(error, queuer, nil)
case .executing(let executor, let requestState, .buffering(_, next: .eof)):
self.state = .executing(executor, requestState, .buffering(.init(), next: .error(error)))
return .cancelExecutor(executor)
case .executing(let executor, _, .buffering(_, next: .askExecutorForMore)):
self.state = .finished(error: error)
return .failTask(error, nil, executor)
case .executing(let executor, _, .buffering(_, next: .error(_))):
// this would override another error, let's keep the first one
return .cancelExecutor(executor)
case .executing(let executor, _, .initialized),
.executing(let executor, _, .waitingForRemote),
.executing(let executor, _, .endReceived):
self.state = .finished(error: error)
return .failTask(error, nil, executor)
case .redirected:
self.state = .finished(error: error)
return .failTask(error, nil, nil)
case .finished(.none):
// An error occurred after the request has finished. Ignore...
return .none
case .deadlineExceededWhileQueued:
let realError: Error = {
if (error as? HTTPClientError) == .cancelled {
/// if we just get a `HTTPClientError.cancelled` we can use the original cancellation reason
/// to give a more descriptive error to the user.
return HTTPClientError.deadlineExceeded
} else {
/// otherwise we already had an intermediate connection error which we should present to the user instead
return error
}
}()
self.state = .finished(error: realError)
return .failTask(realError, nil, nil)
case .finished(.some(_)):
// this might happen, if the stream consumer has failed... let's just drop the data
return .none
case .modifying:
preconditionFailure("Invalid state: \(self.state)")
}
}
}
extension HTTPResponseHead {
var contentLength: Int? {
guard let header = self.headers.first(name: "content-length") else {
return nil
}
return Int(header)
}
}