caddyhttp: add {http.request.proto_name} placeholder for spec-compliant protocol names (#7782)

* caddyhttp: add {http.request.proto_name} placeholder for spec-compliant protocol names

{http.request.proto} exposes Go's raw http.Request.Proto field which
returns HTTP/2.0 and HTTP/3.0 for HTTP/2 and HTTP/3 respectively.
These strings are non-standard since the specs define them as HTTP/2
and HTTP/3.

To preserve backward compat (especially CGI/FastCGI expectations),
{http.request.proto} is kept as-is. A new {http.request.proto_name}
placeholder is introduced that normalises the version string to the
spec-defined form:
  HTTP/2.0 -> HTTP/2
  HTTP/3.0 -> HTTP/3
  all others returned unchanged

Closes #7734

* caddyhttp: Use ProtoMajor for proto_name normalization and update docs

---------

Co-authored-by: jalikajalika5 <105954036+jalikajalika5@users.noreply.github.com>
pull/7799/head^2
Muhammad Syafri, S.Kom 2026-06-04 22:03:19 +07:00 committed by GitHub
parent fcc7860d03
commit 915793f6e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 40 additions and 1 deletions

View File

@ -70,7 +70,8 @@ func init() {
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
// `{http.request.proto}` | The raw protocol of the request as returned by Go (e.g., HTTP/2.0 or HTTP/3.0)
// `{http.request.proto_name}` | The spec-defined protocol of the request (e.g., HTTP/2 or HTTP/3)
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
// `{http.request.local.port}` | The port part of the local address the connection arrived on
// `{http.request.local}` | The local address the connection arrived on

View File

@ -105,6 +105,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return "http", true
case "http.request.proto":
return req.Proto, true
case "http.request.proto_name":
if req.ProtoMajor == 2 {
return "HTTP/2", true
}
if req.ProtoMajor == 3 {
return "HTTP/3", true
}
return req.Proto, true
case "http.request.host":
host, _, err := net.SplitHostPort(req.Host)
if err != nil {

View File

@ -266,3 +266,33 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
}
}
}
func TestHTTPProtoNameNormalization(t *testing.T) {
for _, tc := range []struct {
proto string
major int
expectRaw string
expectName string
}{
{proto: "HTTP/1.0", major: 1, expectRaw: "HTTP/1.0", expectName: "HTTP/1.0"},
{proto: "HTTP/1.1", major: 1, expectRaw: "HTTP/1.1", expectName: "HTTP/1.1"},
{proto: "HTTP/2.0", major: 2, expectRaw: "HTTP/2.0", expectName: "HTTP/2"},
{proto: "HTTP/3.0", major: 3, expectRaw: "HTTP/3.0", expectName: "HTTP/3"},
} {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Proto = tc.proto
req.ProtoMajor = tc.major
repl := caddy.NewReplacer()
addHTTPVarsToReplacer(repl, req, nil)
gotRaw, okRaw := repl.GetString("http.request.proto")
if !okRaw || gotRaw != tc.expectRaw {
t.Errorf("proto=%s: expected http.request.proto to be %q, got %q (ok=%t)", tc.proto, tc.expectRaw, gotRaw, okRaw)
}
gotName, okName := repl.GetString("http.request.proto_name")
if !okName || gotName != tc.expectName {
t.Errorf("proto=%s: expected http.request.proto_name to be %q, got %q (ok=%t)", tc.proto, tc.expectName, gotName, okName)
}
}
}