swift-server / swift-server/async-http-client
Crash in `HTTP2ClientRequestHandler`
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 1.1k
- Forks
- 156
- PR merge metrics
- No merged PRs in 30d
Description
Despite what the comment says, this can crash on the client if the server sends a GOAWAY or RST_STREAM at the right time:
AI Disclosure: yes I used Gemini.
I asked Gemini to write the repro and the following comments, but I reviewed them myself. I found the initial problem while working on the SDK for Google Cloud Storage. That is artisanal, lovingly hand-crafted code, not AI slop. Reducing the problem to a smaller repro did involve LLMs.
A reproducer for a crash in swift-server/async-http-client (HTTP2ClientRequestHandler.swift:249: Fatal error: Unexpectedly found nil while unwrapping an Optional value).
This package runs an in-process mock HTTP/2 server on 127.0.0.1 and reproduces the crash without external network requests.
How to Run
cd HTTP2ClientCrashRepro
swift run
Or run with an explicit trigger action:
# Test stream reset (RST_STREAM) after early response headers (default)
swift run HTTP2ClientCrashRepro rst
# Test connection termination (GOAWAY) after early response headers
swift run HTTP2ClientCrashRepro goaway
Crash Trace
=== Starting HTTP2ClientCrashRepro ===
Testing isolated HTTP/2 repro without external network calls...
Configured server action: respondThenRstStream(errorCode: HTTP2ErrorCode<0x8 Cancel>)
Mock HTTP/2 Server listening at https://127.0.0.1:39483
Sending streaming request to mock server...
AsyncHTTPClient/HTTP2ClientRequestHandler.swift:249: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Response received: 200 OK
💣 Program crashed: Signal 4: Backtracing from 0x7f...
Root Cause Analysis
The crash occurs when the server returns a complete response while the client's request body is still streaming, followed by a stream reset (RST_STREAM) or connection closure (GOAWAY):
- Streaming Request Initiated:
The client begins an HTTP/2 request with a streaming body (HTTPClientRequest.Body.stream). - Server Responds Early:
The server sends response headers (200 OK,412 Precondition Failed, etc.) withendStream: true. - Response Delivered & Request Cleared:
HTTP2ClientRequestHandler.run(.forwardResponseEnd)delivers the response to the caller and clears its stored request reference:// Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift:234 self.request = nil - Server Resets Stream:
While the client's request body is still streaming, the server issues aRST_STREAMframe. - Stream Error Forwarded:
SwiftNIO delivers the stream reset toerrorCaught. - State Machine Returns
.failRequest:
Because the request state machine was in.running(.streaming(...), .endReceived), it transitions to.failedand returns the action.failRequest(error, ...). - Force-Unwrap Crash:
HTTP2ClientRequestHandler.run(.failRequest)force-unwrapsself.request!:// Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift:249 case .failRequest(let error, let finalAction): self.request!.fail(error) // CRASH: self.request was set to nil in step 3
Why TLS / HTTPS Is Required
AsyncHTTPClient does not implement cleartext HTTP/2 (h2c / prior knowledge). In HTTPConnectionPool+Factory.swift (lines 256 and 386), all plain http:// schemes use HTTP/1.1:
private func makePlainChannel(...) {
...
bootstrap.connect(target: self.key.connectionTarget).map {
.http1_1($0)
}.cascade(to: promise)
}
When connecting to http://, AsyncHTTPClient sends HTTP/1.1 text (POST /test HTTP/1.1\r\n...). An HTTP/2 server expecting the HTTP/2 client connection preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n) rejects this with invalid constant string.
In AsyncHTTPClient, HTTP2ClientRequestHandler is only added when ALPN negotiates "h2" during TLS (HTTPConnectionPool+Factory.swift:428). Therefore, an in-process TLS server with ALPN is required to exercise HTTP/2 and reproduce this bug.
Test Certificates
The certificate and private key in Sources/main.swift are test credentials:
- They are generated for loopback testing on
127.0.0.1and not used in production. - The client sets
config.tlsConfiguration?.certificateVerification = .noneto accept the local test certificate. - They are safe to check into source control.
Why This Matters for Google Cloud & HTTP/2 SDKs
In Google Cloud SDKs (such as Cloud Storage):
- Streaming Uploads: Resumable uploads and multipart object uploads stream request bodies via
HTTPClientRequest.Body.stream. - Early Server Responses: If an upload fails early (e.g.
412 Precondition FailedonifGenerationMatch, quota exhaustion, or authorization failure), Cloud Storage closes or resets the stream before the client completes writing the request body. - Process Termination: Rather than delivering an error to the caller (so that retry or recovery logic can execute),
AsyncHTTPClienttriggers a fatal error (SIGILL), crashing the process.
System & Environment Information
The crash was captured and reproduced in the following environment:
| Property | Value |
|---|---|
| Swift Version | Swift version 6.3.3 (swift-6.3.3-RELEASE) / Target: x86_64-unknown-linux-gnu |
| Operating System | Debian GNU/Linux rodete (VERSION_CODENAME=rodete) |
| Linux Kernel | 6.18.14-1rodete4-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.18.14-1rodete4 (2026-06-15) x86_64 |
| C Library (glibc) | ldd (Debian GLIBC 2.43-4+gl0) 2.43 |
| Architecture | x86_64 |
Available CPUs (nproc) |
64 (AMD EPYC 7B13, 1 socket, 32 cores, 64 threads) |
| System Memory | 117 GiB RAM (Total) / ~102 GiB (Available) |
Tested Dependency Versions
| Package | Pinned Version | Revision |
|---|---|---|
swift-server/async-http-client |
1.36.0 |
9544287b9416c0bc71e58b9f3aead8dd14b16103 |
apple/swift-nio |
2.101.3 |
0b18836bd8b0162e7e17a995a3fbee20ed8f3b2b |
apple/swift-nio-http2 |
1.45.0 |
45bdf670248be5f16ec0340e125dca285536f0fb |
apple/swift-nio-ssl |
2.37.2 |
d930168b86f46ca51a4bc09c5ca45c1833db8067 |
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the attached HTTP2ClientCrashRepro with the rst and goaway actions to confirm the failure. Inspect Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift around lines 234–249 and follow the state-machine path into failRequest. Done means both HTTP/2 scenarios report an error without a fatal crash.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- api, networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100