Skip to content

HTTPS proxy hangs on HTTP/2 client requests; works over HTTP/1.1 #2260

@Maxwell2022

Description

@Maxwell2022

Describe the issue

MockServer hangs for ~30 seconds and then sends a graceful HTTP/2 GOAWAY (NO_ERROR, last_stream=1) without sending any response, when an HTTPS request is made through the proxy port (1080) over HTTP/2. The same request works correctly when forced to HTTP/1.1.

This affects both:

  • requests matching a configured expectation (mocked response), and
  • requests with no matching expectation (forward / catch-all behaviour).

The hang happens after MockServer accepts the CONNECT, completes TLS, negotiates h2 via ALPN, and receives the HTTP/2 GET frame. It then never writes a response. After the timeout it sends GOAWAY error=0 and closes the stream, causing curl to error with (16) Error in the HTTP2 framing layer.

What you are trying to do

Use MockServer as the HTTPS forward proxy for a Playwright-based e2e test suite. The browser (headless Chromium) connects to MockServer via proxy: { server: 'http://mockserver:1080' } and Chromium negotiates h2 with the proxy. Some requests should be served by MockServer expectations (e.g. payment-gateway stubs), others should be forwarded upstream. Both paths fail over HTTP/2.

MockServer version

mockserver/mockserver:5.15.0 upgraded to v6 (same image repo, latest v6 tag). Same behaviour on both — v5 fails on upstream HTTP/2, v6 fails on the proxy-facing HTTP/2 response path.

To Reproduce

  1. Run MockServer via Docker (minimal repro, no docker-compose needed):
# CA + key for TLS MITM (any CA works; using mkcert here for simplicity)
mkdir -p ./ca
mkcert -install
cp "$(mkcert -CAROOT)/rootCA.pem" ./ca/cert.pem
cp "$(mkcert -CAROOT)/rootCA-key.pem" ./ca/key.pem

# Expectations: one mock for a known host
mkdir -p ./expectations
cat > ./expectations/typekit.json <<'EOF'
  [
    {
      "id": "typekit-stub-css",
      "priority": 10,
      "httpRequest": {
        "method": "GET",
        "path": "/mzn8ecb.css",
        "headers": { "Host": ["use\\.typekit\\.net"] }
      },
      "httpResponse": {
        "statusCode": 200,
        "headers": { "Content-Type": ["text/css;charset=utf-8"] },
        "body": "/* mocked */"
      }
    }
  ]
  EOF


docker run --rm --name mockserver \
    -p 1080:1080 \
    -v "$PWD/ca:/config/ca:ro" \
    -v "$PWD/expectations:/config/expectations:ro" \
    -e MOCKSERVER_SERVER_PORT="1080,443" \
    -e MOCKSERVER_LOG_LEVEL="INFO" \
    -e MOCKSERVER_CERTIFICATE_AUTHORITY_X509_CERTIFICATE="/config/ca/cert.pem" \
    -e MOCKSERVER_CERTIFICATE_AUTHORITY_PRIVATE_KEY="/config/ca/key.pem" \
    -e MOCKSERVER_INITIALIZATION_JSON_PATH="/config/expectations/*.json" \
    mockserver/mockserver:6.0.0 # or 5.15.0
  1. Reproduce — HTTPS over HTTP/2 (hangs):
curl -kx http://localhost:1080 -v 'https://use.typekit.net/mzn8ecb.css' \
  -o /dev/null --max-time 35 2>&1 | tail -20

  Output (abridged):
  * using HTTP/2
  * [HTTP/2] [1] OPENED stream for https://use.typekit.net/mzn8ecb.css
  * [HTTP/2] [1] [:method: GET]
  * [HTTP/2] [1] [:authority: use.typekit.net]
  * [HTTP/2] [1] [:path: /mzn8ecb.css]
  > GET /mzn8ecb.css HTTP/2
  * Request completely sent off
  * received GOAWAY, error=0, last_stream=1   <-- ~29s later
  * TLSv1.2 (IN), TLS alert, close notify (256):
  curl: (16) Error in the HTTP2 framing layer
  1. Compare — same request forced to HTTP/1.1 (works):
  curl -kx http://localhost:1080 --http1.1 -v 'https://use.typekit.net/mzn8ecb.css' \
    -o /dev/null --max-time 35 2>&1 | tail -20

  3. Output:
  < HTTP/1.1 200 OK
  < Content-Type: text/css;charset=utf-8
  < content-length: 12
  { [12 bytes data]
  /* mocked */

The only variable between the two runs is ALPN negotiating h2 vs http/1.1. The mocked expectation matches in both cases, only HTTP/2 hangs.

Expected behaviour

When the client negotiates h2 via ALPN on the proxy port, MockServer should:

  • send the mocked httpResponse back as a valid HTTP/2 response on the same stream, or
  • if no expectation matches and forwarding is desired, complete the forward and relay the upstream response as HTTP/2.

Either outcome is acceptable; silently hanging and emitting GOAWAY after ~30s is not.

MockServer Log

At MOCKSERVER_LOG_LEVEL=INFO (and at lower levels), no log line is emitted for the HTTP/2 request — neither a "received request" entry nor an "expectation matched" entry.

The mockserver/retrieve?type=LOGS&format=JSON endpoint also returns no record of the request. From the operator's perspective the request silently disappears inside MockServer. The HTTP/1.1 variant of the same request does produce normal INFO-level log entries showing the expectation matching.

  (Container startup logs only contain SLF4J init noise:)
  SLF4J(W): No SLF4J providers were found.
  SLF4J(W): Defaulting to no-operation (NOP) logger implementation
  SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
  Loading JavaScript to validate ECMA262 regular expression in JsonSchema ...

The absence of an SLF4J binding in the official image (v6.0.0) is likely a separate packaging issue but is worth flagging here as it makes the HTTP/2 hang impossible to diagnose from container logs alone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions