ProxyMapService now supports HTTP/1.1 → HTTP/2 translation: the client continues to speak HTTP/1.1, while the proxy communicates with the upstream server over h2 and converts responses back to HTTP/1.1.
Why it’s useful
Many modern servers (gRPC gateways, Kestrel configured for h2-only, and CDNs) accept or prefer HTTP/2. At the same time, client applications, scripts, and older libraries often support only HTTP/1.1. The proxy bridges this gap: the client needs no changes, while the server gets to use a modern protocol. All traffic still passes through the proxy and is logged, including h2 frames on the server side.
How to enable it
The EnableTranslateH1ToH2 flag can be set at three levels: globally, for a listening port, or for an individual host rule:
{
"HTTP": {
"EnableTranslateH1ToH2": true
},
"ProxyMappings": [
{
"Listen": {
"Port": 8080,
"Action": "Bypass",
"DecryptSSL": true,
"EnableTranslateH1ToH2": true
}
}
],
"HostRules": {
"Items": [
{
"HostName": "api.example.com",
"DecryptSSL": true,
"EnableTranslateH1ToH2": true
}
]
}
}
Translation works only for HTTPS connections with decryption (MITM) enabled: the proxy needs to inspect requests in order to repackage them into frames. This flag has no effect on transparent tunnels or regular HTTP.
Important: the flag alone does not do anything — the upstream server must select h2 during the TLS handshake. Therefore, h2 must be included in SslClientOptions.ApplicationProtocols (see below).
How it works and limitations
Each client request is parsed as HTTP/1.1. The proxy then sends HEADERS and DATA frames to the server and converts the h2 response back into a status line, headers, and a body. The conversion rules are:
- Hop-by-hop headers (
Connection,Keep-Alive,Transfer-Encoding,Upgrade,Host) are removed;Hostbecomes the:authoritypseudo-header. - The method, scheme, and path are mapped to
:method,:scheme, and:path. - If the response has no
Content-Length, the client receives it withTransfer-Encoding: chunked; trailers are forwarded. - Invalid headers (CR/LF/NUL, conflicting
Content-Lengthvalues, or ambiguousTransfer-Encoding) are rejected to protect against request smuggling.
Limitations:
- Only h1 → h2 translation is supported; the reverse direction, h2 → h1, is not.
- Only one h2 stream is active at a time; client pipelined requests are processed sequentially.
CONNECTand WebSocket upgrades are not translated and receive a501 Not Implementedresponse.- Server push is disabled (
ENABLE_PUSH=0). - If the server sends
RST_STREAM, the client receives a502response.
The role of ApplicationProtocols
The proxy performs two TLS handshakes, each with its own ALPN list. First, three terms:
- C —
SslClientOptions.ApplicationProtocols: what the proxy offers to the upstream server. - S —
SslServerOptions.ApplicationProtocols: what the proxy allows the client to use. - K — the ALPN list in the client’s own ClientHello: the protocols the client actually supports.
The handshakes happen in reverse order: the proxy negotiates with the upstream server first and then responds to the client. More precisely, it peeks at the ClientHello in advance (SocketFlags.Peek) and uses that information to decide what to offer the server.
If a list is empty (in K or S), only http/1.1 is assumed. An empty ALPN list means the client does not negotiate a protocol, so HTTP/1.1 is used over TLS.
The decision is made in three steps (T means translation is enabled):
- Determine client capabilities:
h1 = K ∩ S contains http/1.1,h2 = K ∩ S contains h2. - Decide what to offer upstream: offer
h2if it is present in C and (the client supports h2, or T is enabled and the client supports h1); offerhttp/1.1if it is present in C and the client supports h1. - After the upstream responds: if it selects h2 and the client supports h2, use transparent HTTP/2; if it selects h2 and the client supports only h1 while T is enabled, translate h1 → h2; if it selects
http/1.1and the client supports h1, use regular HTTP/1.1. Any other combination results in a negotiation error.
The proxy records the outcome and offers the client exactly one protocol — the one that will be used for the session.
Scenarios
A browser sends K = http/1.1, h2 unless configured otherwise. curl and similar clients offer only http/1.1.
C (SslClientOptions) |
S (SslServerOptions) |
K (client) | T | Result |
|---|---|---|---|---|
| http/1.1 | http/1.1 | Any client supporting h1 | Either | HTTP/1.1 on both sides |
| http/1.1,h2 | http/1.1,h2 | http/1.1,h2 | Either | Uses the protocol selected by the server: transparent HTTP/2 if h2 is selected; otherwise HTTP/1.1 |
| h2 | http/1.1 | http/1.1 | Enabled | h1 → h2 translation |
| h2 | http/1.1 | http/1.1 | Disabled | Negotiation error |
| h2 | http/1.1,h2 | http/1.1,h2 | Either | Transparent HTTP/2; translation is not needed |
| h2 | http/1.1,h2 | http/1.1 (curl) | Enabled | h1 → h2 translation |
| h2 | http/1.1,h2 | http/1.1 (curl) | Disabled | Negotiation error |
| http/1.1,h2 | http/1.1 | http/1.1,h2 | Enabled | If upstream selects h2, translate; if it selects http/1.1, use regular HTTP/1.1 |
| http/1.1,h2 | http/1.1 | http/1.1,h2 | Disabled | HTTP/1.1 on both sides (h2 is not offered upstream) |
| http/1.1 | h2 | http/1.1,h2 | Either | Error: the client supports only h2, but h2 is not offered upstream (there is no h2 → h1 translation) |
The last row is a common trap: http/1.1 in C and a strict h2 in S are incompatible under any settings, because translation is supported in only one direction.
Diagnostics
When translation succeeds, the log contains an Info-level entry:
HTTP/1.1 -> HTTP/2 translation: client 127.0.0.1:39461 speaks HTTP/1.1, upstream localhost:5112 negotiated h2
A good starting configuration is: SslClientOptions.ApplicationProtocols = "http/1.1,h2", SslServerOptions.ApplicationProtocols = "http/1.1", and EnableTranslateH1ToH2 = true. The proxy will offer h2 to the server while keeping the client on HTTP/1.1.