swift-server / swift-server/async-http-client

Crash in `HTTP2ClientRequestHandler`

Open
#922 0 comments 0 reactions 0 assignees View on GitHub

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:

https://github.com/swift-server/async-http-client/blob/f95c908967e98c68c5ce3fd61a7974e7e869e303/Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift#L247-L249

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):

  1. Streaming Request Initiated:
    The client begins an HTTP/2 request with a streaming body (HTTPClientRequest.Body.stream).
  2. Server Responds Early:
    The server sends response headers (200 OK, 412 Precondition Failed, etc.) with endStream: true.
  3. 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
    
  4. Server Resets Stream:
    While the client's request body is still streaming, the server issues a RST_STREAM frame.
  5. Stream Error Forwarded:
    SwiftNIO delivers the stream reset to errorCaught.
  6. State Machine Returns .failRequest:
    Because the request state machine was in .running(.streaming(...), .endReceived), it transitions to .failed and returns the action .failRequest(error, ...).
  7. 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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.