Despite what the comment says, this can crash on the client if the server sends a GOAWAY or RST_STREAM at the right time:
|
// We can force unwrap the request here, as we have just validated in the state machine, |
|
// that the request object is still present. |
|
self.request!.fail(error) |
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.
HTTP2ClientCrashRepro.tar.gz
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.) with endStream: 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 a RST_STREAM frame.
- Stream Error Forwarded:
SwiftNIO delivers the stream reset to errorCaught.
- State Machine Returns
.failRequest:
Because the request state machine was in .running(.streaming(...), .endReceived), it transitions to .failed and returns the action .failRequest(error, ...).
- Force-Unwrap Crash:
HTTP2ClientRequestHandler.run(.failRequest) force-unwraps self.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.1 and not used in production.
- The client sets
config.tlsConfiguration?.certificateVerification = .none to 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 Failed on ifGenerationMatch, 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),
AsyncHTTPClient triggers 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 |
Despite what the comment says, this can crash on the client if the server sends a GOAWAY or RST_STREAM at the right time:
async-http-client/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
Lines 247 to 249 in f95c908
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.
HTTP2ClientCrashRepro.tar.gz
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.1and reproduces the crash without external network requests.How to Run
cd HTTP2ClientCrashRepro swift runOr run with an explicit trigger action:
Crash Trace
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):The client begins an HTTP/2 request with a streaming body (
HTTPClientRequest.Body.stream).The server sends response headers (
200 OK,412 Precondition Failed, etc.) withendStream: true.HTTP2ClientRequestHandler.run(.forwardResponseEnd)delivers the response to the caller and clears its stored request reference:While the client's request body is still streaming, the server issues a
RST_STREAMframe.SwiftNIO delivers the stream reset to
errorCaught..failRequest:Because the request state machine was in
.running(.streaming(...), .endReceived), it transitions to.failedand returns the action.failRequest(error, ...).HTTP2ClientRequestHandler.run(.failRequest)force-unwrapsself.request!:Why TLS / HTTPS Is Required
AsyncHTTPClientdoes not implement cleartext HTTP/2 (h2c/ prior knowledge). InHTTPConnectionPool+Factory.swift(lines 256 and 386), all plainhttp://schemes use HTTP/1.1:When connecting to
http://,AsyncHTTPClientsends 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 withinvalid constant string.In
AsyncHTTPClient,HTTP2ClientRequestHandleris 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.swiftare test credentials:127.0.0.1and not used in production.config.tlsConfiguration?.certificateVerification = .noneto accept the local test certificate.Why This Matters for Google Cloud & HTTP/2 SDKs
In Google Cloud SDKs (such as Cloud Storage):
HTTPClientRequest.Body.stream.412 Precondition FailedonifGenerationMatch, quota exhaustion, or authorization failure), Cloud Storage closes or resets the stream before the client completes writing the request body.AsyncHTTPClienttriggers a fatal error (SIGILL), crashing the process.System & Environment Information
The crash was captured and reproduced in the following environment:
Swift version 6.3.3 (swift-6.3.3-RELEASE)/ Target:x86_64-unknown-linux-gnuDebian GNU/Linux rodete(VERSION_CODENAME=rodete)6.18.14-1rodete4-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.18.14-1rodete4 (2026-06-15) x86_64ldd (Debian GLIBC 2.43-4+gl0) 2.43x86_64nproc)64(AMD EPYC 7B13, 1 socket, 32 cores, 64 threads)117 GiB RAM(Total) /~102 GiB(Available)Tested Dependency Versions
swift-server/async-http-client1.36.09544287b9416c0bc71e58b9f3aead8dd14b16103apple/swift-nio2.101.30b18836bd8b0162e7e17a995a3fbee20ed8f3b2bapple/swift-nio-http21.45.045bdf670248be5f16ec0340e125dca285536f0fbapple/swift-nio-ssl2.37.2d930168b86f46ca51a4bc09c5ca45c1833db8067