Skip to content

[bug]: ALPN negotiation blocks HTTP/2 upgrade for HTTP/1.1 requests with Upgrade header #3898

@zesty-clawd

Description

@zesty-clawd

Bug Description

The proxy's ALPN negotiation logic incorrectly excludes h2 from NextProtos when it detects an HTTP/1.1 request line, even when the client explicitly requests HTTP/2 upgrade via Upgrade: h2c headers. This prevents legitimate HTTP/2 protocol upgrades.

Affected Code

File: pkg/agent/proxy/proxy.go, lines ~917-927

nextProtos := []string{"http/1.1"} // default safe

isHTTP := util.IsHTTPReq(initialBuf)
isCONNECT := bytes.HasPrefix(initialBuf, []byte("CONNECT "))

// Allow H2 if:
// 1. It's not an HTTP/1.x request (could be gRPC/HTTP2 frames), OR
// 2. It's a CONNECT request (used by gRPC parser for tunneling, ALB requires H2)
if !isHTTP || isCONNECT {
    nextProtos = []string{"h2", "http/1.1"}
} else {
    // BUG: Never offers h2 for standard HTTP/1.1 requests
    logger.Debug("NOT offering H2 (HTTP/1.x detected)", zap.Strings("nextProtos", nextProtos))
}

Root Cause

The condition !isHTTP || isCONNECT means:

  • If isHTTP is true (HTTP/1.1 request detected) AND isCONNECT is false, then nextProtos remains ["http/1.1"].
  • This blocks HTTP/2 negotiation even when:
    • The client sends Upgrade: h2c header (explicit HTTP/2 upgrade request)
    • Both client and server fully support HTTP/2
    • ALPN negotiation should offer h2 to enable protocol switch

Impact

1. HTTP/2 Upgrade Failures

Services using HTTP/1.1 → HTTP/2 upgrade mechanisms fail to negotiate HTTP/2:

  • AWS Application Load Balancer (ALB)
  • Some gRPC-Web implementations
  • Cloudflare Workers
  • Any service that uses Upgrade: h2c for protocol negotiation

2. Performance Degradation

  • Forces HTTP/1.1 even when HTTP/2 is mutually supported
  • Loss of HTTP/2 multiplexing, header compression, server push
  • Increased latency and bandwidth usage

3. Silent Failures

  • No error is thrown — the proxy silently downgrades to HTTP/1.1
  • Developers may not realize their HTTP/2 services are running on HTTP/1.1
  • Harder to debug than explicit failures

Reproduction

  1. Deploy a service that sends HTTP/1.1 requests with Upgrade: h2c:

    GET /api HTTP/1.1
    Host: example.com
    Upgrade: h2c
    Connection: Upgrade, HTTP2-Settings
    HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
    
  2. Proxy the request through Keploy with TLS enabled

  3. Check ALPN negotiation logs

Expected: ALPN offers ["h2", "http/1.1"] and successfully negotiates h2
Actual: ALPN only offers ["http/1.1"], preventing HTTP/2 upgrade

Suggested Fix

Option 1: Always Offer HTTP/2 (Recommended)

// Always offer H2 in ALPN for TLS connections (clients that don't support it will ignore)
nextProtos := []string{"h2", "http/1.1"}

if !isHTTP || isCONNECT {
    logger.Debug("Offering H2 (prioritized for gRPC/CONNECT)", zap.Strings("nextProtos", nextProtos))
} else {
    logger.Debug("Offering H2 (standard HTTP/1.1 with upgrade support)", zap.Strings("nextProtos", nextProtos))
}

Rationale:

  • ALPN is a negotiation — offering h2 doesn't force its use if the server doesn't support it
  • Modern best practice: always offer HTTP/2 in TLS handshakes
  • The current "safe" default (http/1.1 only) is unsafe because it breaks upgrade scenarios

Option 2: Conservative Fix (Detect Upgrade Intent)

If backward compatibility is critical:

nextProtos := []string{"http/1.1"}

// Check for HTTP/2 upgrade intent in headers
if !isHTTP || isCONNECT || bytes.Contains(initialBuf, []byte("Upgrade: h2c")) {
    nextProtos = []string{"h2", "http/1.1"}
    logger.Debug("Offering H2 for ALPN", zap.Strings("nextProtos", nextProtos))
}

Additional Context

  • No test coverage exists for this scenario (checked with grep -r "Upgrade.*h2c")
  • HTTP/2 upgrade is part of RFC 9113 (Section 3.2)
  • ALPN (RFC 7301) recommends clients offer all supported protocols

Environment

  • Keploy version: v3 (main branch)
  • OS: All platforms (protocol-level bug)
  • Go version: Any

Would you like me to submit a PR with the fix?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions