Caddy not accepting placeholder headers when reverse proxying websockets to Jetty

1. The problem I’m having:

Caddy is not pushing correct headers using placeholders when reverse proxying to jetty websocket server. Specifically header_up Sec-WebSocket-Key header. When I’m hardcoding the value, it does work.

I want to implement jwt validation and only then I want to proxy the same request to my jetty server which is listening for active websockets on port 8090. Also websocket is listening on the path "/ws"

Caddy is build using xcaddy on the module GitHub - ggicci/caddy-jwt: 🆔 Caddy Module JWT Authentication

2. Error messages and/or full log output:

Notice the differences in config file

  • Hardcoded Sec-WebSocket-Key websocket headers - they work
{
order jwtauth before reverse_proxy
debug
}

localhost:8080 {
route /ws {
jwtauth {
sign_key YW1pZ28=
sign_alg HS256
user_claims name
}

reverse_proxy localhost:8090 {

header_up Host {http.request.host} # Explicit Host value
header_up X-Real-IP {http.request.remote.host} # Explicit IP value (localhost IPv6)
header_up X-Forwarded-For {http.request.remote.host} # Explicit IP value (localhost IPv6)
header_up X-Forwarded-Port {http.request.port} # Explicit port value
header_up X-Forwarded-Proto {http.request.scheme} # Explicit protocol
header_up Sec-WebSocket-Version 13
header_up Sec-WebSocket-Key n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4
# header_up Sec-WebSocket-Key {>Sec-WebSocket-Key}
header_up Upgrade websocket # Explicit Upgrade value
header_up Connection Upgrade # Explicit Connection value

}
}
}



After using caddy.exe run

I send this request via curl

curl -vvvkl "https://localhost:8080/ws" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaG9sYSJ9.28_fvE4ygYXjzL9CbxXgBR2ZdCWmnOXCO8tvYAI4eq0"

o/p

* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* using HTTP/1.x
> GET /ws HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.9.1
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaG9sYSJ9.28_fvE4ygYXjzL9CbxXgBR2ZdCWmnOXCO8tvYAI4eq0
>
* Request completely sent off
< HTTP/1.1 101 Switching Protocols
< Alt-Svc: h3=":8080"; ma=2592000
< Connection: Upgrade
< Date: Wed, 02 Apr 2025 17:58:22 GMT
< Sec-WebSocket-Accept: WShj5nKJRfyS7XVd4/uOdjqrCNE=
< Server: Caddy
< Server: Jetty(12.0.16)
< Upgrade: websocket
<

Caddy Logs

2025/04/02 17:55:14.013 ←[34mINFO←[0m   using adjacent Caddyfile
2025/04/02 17:55:14.014 ←[33mWARN←[0m   caddyfile       Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream
2025/04/02 17:55:14.014 ←[33mWARN←[0m   caddyfile       Unnecessary header_up X-Forwarded-Proto: the reverse proxy's default behavior is to pass headers to the upstream
2025/04/02 17:55:14.015 ←[34mINFO←[0m   adapted config to JSON  {"adapter": "caddyfile"}
2025/04/02 17:55:14.016 ←[33mWARN←[0m   Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies    {"adapter": "caddyfile", "file": "Caddyfile", "line": 2}
2025/04/02 17:55:14.026 ←[34mINFO←[0m   admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//[::1]:2019", "//127.0.0.1:2019", "//localhost:2019"]}
2025/04/02 17:55:14.026 ←[34mINFO←[0m   tls.cache.maintenance   started background certificate maintenance      {"cache": "0xc00064f800"}
2025/04/02 17:55:14.026 ←[34mINFO←[0m   http.auto_https enabling automatic HTTP->HTTPS redirects        {"server_name": "srv0"}
2025/04/02 17:55:14.027 ←[35mDEBUG←[0m  http.auto_https adjusted config {"tls": {"automation":{"policies":[{"subjects":["localhost"]},{}]}}, "http": {"servers":{"remaining_auto_https_redirects":{"listen":[":80"],"routes":[{},{}]},"srv0":{"listen":[":8080"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"authentication","providers":{"jwt":{"audience_whitelist":null,"from_cookies":null,"from_header":null,"from_query":null,"issuer_whitelist":null,"jwk_url":"","meta_claims":null,"sign_alg":"HS256","sign_key":"YW1pZ28=","skip_verification":false,"user_claims":["name"]}}},{"handler":"reverse_proxy","headers":{"request":{"set":{"Connection":["Upgrade"],"Host":["{http.request.host}"],"Sec-Websocket-Key":["n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4"],"Sec-Websocket-Version":["13"],"Upgrade":["websocket"],"X-Forwarded-For":["{http.request.remote.host}"],"X-Forwarded-Port":["{http.request.port}"],"X-Forwarded-Proto":["{http.request.scheme}"],"X-Real-Ip":["{http.request.remote.host}"]}}},"upstreams":[{"dial":"localhost:8090"}]}]}]}],"match":[{"path":["/ws"]}]}]}],"terminal":true}],"tls_connection_policies":[{}],"automatic_https":{}}}}}
2025/04/02 17:55:14.030 ←[34mINFO←[0m   tls     storage cleaning happened too recently; skipping for now        {"storage": "FileStorage:C:\\Users\\username\\AppData\\Roaming\\Caddy", "instance": "49e59ef7-a3f2-4fd3-a466-4f510b2ddf53", "try_again": "2025/04/03 17:55:14.030", "try_again_in": 86400}
2025/04/02 17:55:14.030 ←[34mINFO←[0m   tls     finished cleaning storage units
2025/04/02 17:55:14.042 ←[34mINFO←[0m   pki.ca.local    root certificate is already trusted by system   {"path": "storage:pki/authorities/local/root.crt"}
2025/04/02 17:55:14.043 ←[35mDEBUG←[0m  http    starting server loop    {"address": "[::]:8080", "tls": true, "http3": false}
2025/04/02 17:55:14.043 ←[34mINFO←[0m   http    enabling HTTP/3 listener        {"addr": ":8080"}
2025/04/02 17:55:14.043 ←[34mINFO←[0m   http.log        server running  {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2025/04/02 17:55:14.043 ←[35mDEBUG←[0m  http    starting server loop    {"address": "[::]:80", "tls": false, "http3": false}
2025/04/02 17:55:14.043 ←[33mWARN←[0m   http    HTTP/2 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2025/04/02 17:55:14.043 ←[33mWARN←[0m   http    HTTP/3 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2025/04/02 17:55:14.043 ←[34mINFO←[0m   http.log        server running  {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2025/04/02 17:55:14.043 ←[34mINFO←[0m   http    enabling automatic TLS certificate management   {"domains": ["localhost"]}
2025/04/02 17:55:14.044 ←[35mDEBUG←[0m  tls.cache       added certificate to cache      {"subjects": ["localhost"], "expiration": "2025/04/02 23:25:40.000", "managed": true, "issuer_key": "local", "hash": "edc650750ad0172995bdc5bd6741ebb0360839308c679698931e56f91d0283bb", "cache_size": 1, "cache_capacity": 10000}
2025/04/02 17:55:14.044 ←[35mDEBUG←[0m  events  event   {"name": "cached_managed_cert", "id": "58dbc699-b587-4eae-9411-97f592dd4a45", "origin": "tls", "data": {"sans":["localhost"]}}
2025/04/02 17:55:14.045 ←[34mINFO←[0m   autosaved config (load with --resume flag)      {"file": "C:\\Users\\username\\AppData\\Roaming\\Caddy\\autosave.json"}
2025/04/02 17:55:14.045 ←[34mINFO←[0m   serving initial configuration
2025/04/02 17:58:22.365 ←[35mDEBUG←[0m  events  event   {"name": "tls_get_certificate", "id": "e36d88a9-f766-4486-b6d9-323de84360f5", "origin": "tls", "data": {"client_hello":{"CipherSuites":[49196,49195,49200,49199,159,158,49188,49187,49192,49191,49162,49161,49172,49171,157,156,61,60,53,47,10],"ServerName":"localhost","SupportedCurves":[29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[2052,2053,2054,1025,1281,513,1027,1283,515,514,1537,1539],"SupportedProtos":["http/1.1"],"SupportedVersions":[771,770,769],"RemoteAddr":{"IP":"::1","Port":58398,"Zone":""},"LocalAddr":{"IP":"::1","Port":8080,"Zone":""}}}}
2025/04/02 17:58:22.365 ←[35mDEBUG←[0m  tls.handshake   choosing certificate    {"identifier": "localhost", "num_choices": 1}
2025/04/02 17:58:22.365 ←[35mDEBUG←[0m  tls.handshake   default certificate selection results   {"identifier": "localhost", "subjects": ["localhost"], "managed": true, "issuer_key": "local", "hash": "edc650750ad0172995bdc5bd6741ebb0360839308c679698931e56f91d0283bb"}
2025/04/02 17:58:22.365 ←[35mDEBUG←[0m  tls.handshake   matched certificate in cache    {"remote_ip": "::1", "remote_port": "58398", "subjects": ["localhost"], "managed": true, "expiration": "2025/04/02 23:25:40.000", "hash": "edc650750ad0172995bdc5bd6741ebb0360839308c679698931e56f91d0283bb"}
2025/04/02 17:58:22.370 ←[34mINFO←[0m   http.authentication.providers.jwt       user authenticated      {"token_string": "eyJhbGciOiJIUzI1…mnOXCO8tvYAI4eq0", "user_claim": "name", "id": "hola"}
2025/04/02 17:58:22.370 ←[35mDEBUG←[0m  http.handlers.reverse_proxy     selected upstream       {"dial": "localhost:8090", "total_upstreams": 1}
2025/04/02 17:58:22.388 ←[35mDEBUG←[0m  http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "localhost:8090", "duration": 0.0171977, "request": {"remote_ip": "::1", "remote_port": "58398", "client_ip": "::1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost", "uri": "/ws", "headers": {"X-Forwarded-Port": ["8080"], "Sec-Websocket-Version": ["13"], "User-Agent": ["curl/8.9.1"], "Accept": ["*/*"], "Authorization": ["REDACTED"], "X-Forwarded-Host": ["localhost:8080"], "Upgrade": ["websocket"], "X-Real-Ip": ["::1"], "Connection": ["Upgrade"], "X-Forwarded-For": ["::1"], "X-Forwarded-Proto": ["https"], "Sec-Websocket-Key": ["n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4"]}, "tls": {"resumed": false, "version": 771, "cipher_suite": 49195, "proto": "http/1.1", "server_name": "localhost"}}, "headers": {"Server": ["Jetty(12.0.16)"], "Date": ["Wed, 02 Apr 2025 17:58:22 GMT"], "Upgrade": ["websocket"], "Sec-Websocket-Accept": ["WShj5nKJRfyS7XVd4/uOdjqrCNE="], "Connection": ["Upgrade"]}, "status": 101}
2025/04/02 17:58:22.389 ←[35mDEBUG←[0m  http.handlers.reverse_proxy     upgrading connection    {"upstream": "localhost:8090", "duration": 0.0171977, "request": {"remote_ip": "::1", "remote_port": "58398", "client_ip": "::1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost", "uri": "/ws", "headers": {"X-Forwarded-Port": ["8080"], "Sec-Websocket-Version": ["13"], "User-Agent": ["curl/8.9.1"], "Accept": ["*/*"], "Authorization": ["REDACTED"], "X-Forwarded-Host": ["localhost:8080"], "Upgrade": ["websocket"], "X-Real-Ip": ["::1"], "Connection": ["Upgrade"], "X-Forwarded-For": ["::1"], "X-Forwarded-Proto": ["https"], "Sec-Websocket-Key": ["n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4"]}, "tls": {"resumed": false, "version": 771, "cipher_suite": 49195, "proto": "http/1.1", "server_name": "localhost"}}, "http_version": 1}

As you can see from Caddy log [The last line], this is the name:value for the header "Sec-Websocket-Key": ["n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4"]}

Additionally from my jetty logs, I see this header

23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:FIELD --> IN_VALUE
23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:IN_VALUE --> FIELD(Connection: Upgrade: Upgrade)
23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - parsedHeader(Connection: Upgrade) header=Connection, headerString=[Connection], valueString=[Upgrade]
23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:FIELD --> VALUE
23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:VALUE --> IN_VALUE
23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:IN_VALUE --> FIELD(Sec-WebSocket-Key: n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4)
23:28:22.381 [qtp1147580192-56] DEBUG o.eclipse.jetty.http.HttpParser - parsedHeader(null) header=Sec-WebSocket-Key, headerString=[Sec-WebSocket-Key], valueString=[n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4]

So that means Caddy can forward the hardcoded header which is expected.

  • Placeholders websocket headers - the placeholders are being passed as is in the reverse_proxy section. Please note the 3rd last header
{
order jwtauth before reverse_proxy
debug
}

localhost:8080 {
route /ws {
jwtauth {
sign_key YW1pZ28=
sign_alg HS256
user_claims name
}

reverse_proxy localhost:8090 {

header_up Host {http.request.host} # Explicit Host value
header_up X-Real-IP {http.request.remote.host} # Explicit IP value (localhost IPv6)
header_up X-Forwarded-For {http.request.remote.host} # Explicit IP value (localhost IPv6)
header_up X-Forwarded-Port {http.request.port} # Explicit port value
header_up X-Forwarded-Proto {http.request.scheme} # Explicit protocol
header_up Sec-Websocket-Version 13
# header_up Sec-WebSocket-Key n5Y5CpwHDlJv1O6JPW55FFMDNCy7GHI0wehpvAVLB4
header_up Sec-WebSocket-Key {>Sec-WebSocket-Key}
header_up Upgrade websocket # Explicit Upgrade value
header_up Connection Upgrade # Explicit Connection value

}
}
}

o/p from curl

* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* using HTTP/1.x
> GET /ws HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.9.1
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaG9sYSJ9.28_fvE4ygYXjzL9CbxXgBR2ZdCWmnOXCO8tvYAI4eq0
>
* Request completely sent off
< HTTP/1.1 101 Switching Protocols
< Alt-Svc: h3=":8080"; ma=2592000
< Connection: Upgrade
< Date: Wed, 02 Apr 2025 18:05:40 GMT
< Sec-WebSocket-Accept: 8uqguKSXBj9pNIoSSdV7K8mDnXw=
< Server: Caddy
< Server: Jetty(12.0.16)
< Upgrade: websocket
<

Caddy Logs

2025/04/02 18:04:33.856 ←[34mINFO←[0m   using adjacent Caddyfile
2025/04/02 18:04:33.857 ←[33mWARN←[0m   caddyfile       Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream
2025/04/02 18:04:33.857 ←[33mWARN←[0m   caddyfile       Unnecessary header_up X-Forwarded-Proto: the reverse proxy's default behavior is to pass headers to the upstream
2025/04/02 18:04:33.859 ←[34mINFO←[0m   adapted config to JSON  {"adapter": "caddyfile"}
2025/04/02 18:04:33.859 ←[33mWARN←[0m   Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies    {"adapter": "caddyfile", "file": "Caddyfile", "line": 2}
2025/04/02 18:04:33.868 ←[34mINFO←[0m   admin   admin endpoint started  {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2025/04/02 18:04:33.868 ←[34mINFO←[0m   tls.cache.maintenance   started background certificate maintenance      {"cache": "0xc00044eb80"}
2025/04/02 18:04:33.868 ←[34mINFO←[0m   http.auto_https enabling automatic HTTP->HTTPS redirects        {"server_name": "srv0"}
2025/04/02 18:04:33.869 ←[35mDEBUG←[0m  http.auto_https adjusted config {"tls": {"automation":{"policies":[{"subjects":["localhost"]},{}]}}, "http": {"servers":{"remaining_auto_https_redirects":{"listen":[":80"],"routes":[{},{}]},"srv0":{"listen":[":8080"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"authentication","providers":{"jwt":{"audience_whitelist":null,"from_cookies":null,"from_header":null,"from_query":null,"issuer_whitelist":null,"jwk_url":"","meta_claims":null,"sign_alg":"HS256","sign_key":"YW1pZ28=","skip_verification":false,"user_claims":["name"]}}},{"handler":"reverse_proxy","headers":{"request":{"set":{"Connection":["Upgrade"],"Host":["{http.request.host}"],"Sec-Websocket-Key":["{\u003eSec-WebSocket-Key}"],"Sec-Websocket-Version":["13"],"Upgrade":["websocket"],"X-Forwarded-For":["{http.request.remote.host}"],"X-Forwarded-Port":["{http.request.port}"],"X-Forwarded-Proto":["{http.request.scheme}"],"X-Real-Ip":["{http.request.remote.host}"]}}},"upstreams":[{"dial":"localhost:8090"}]}]}]}],"match":[{"path":["/ws"]}]}]}],"terminal":true}],"tls_connection_policies":[{}],"automatic_https":{}}}}}
2025/04/02 18:04:33.884 ←[34mINFO←[0m   pki.ca.local    root certificate is already trusted by system   {"path": "storage:pki/authorities/local/root.crt"}
2025/04/02 18:04:33.884 ←[35mDEBUG←[0m  http    starting server loop    {"address": "[::]:8080", "tls": true, "http3": false}
2025/04/02 18:04:33.885 ←[34mINFO←[0m   http    enabling HTTP/3 listener        {"addr": ":8080"}
2025/04/02 18:04:33.885 ←[34mINFO←[0m   http.log        server running  {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2025/04/02 18:04:33.885 ←[35mDEBUG←[0m  http    starting server loop    {"address": "[::]:80", "tls": false, "http3": false}
2025/04/02 18:04:33.885 ←[33mWARN←[0m   http    HTTP/2 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2025/04/02 18:04:33.885 ←[33mWARN←[0m   http    HTTP/3 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2025/04/02 18:04:33.885 ←[34mINFO←[0m   http.log        server running  {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2025/04/02 18:04:33.885 ←[34mINFO←[0m   http    enabling automatic TLS certificate management   {"domains": ["localhost"]}
2025/04/02 18:04:33.886 ←[35mDEBUG←[0m  tls.cache       added certificate to cache      {"subjects": ["localhost"], "expiration": "2025/04/02 23:25:40.000", "managed": true, "issuer_key": "local", "hash": "edc650750ad0172995bdc5bd6741ebb0360839308c679698931e56f91d0283bb", "cache_size": 1, "cache_capacity": 10000}
2025/04/02 18:04:33.886 ←[35mDEBUG←[0m  events  event   {"name": "cached_managed_cert", "id": "a8e44720-ff0e-4687-87a5-bb03ea5c53fc", "origin": "tls", "data": {"sans":["localhost"]}}
2025/04/02 18:04:33.887 ←[34mINFO←[0m   autosaved config (load with --resume flag)      {"file": "C:\\Users\\username\\AppData\\Roaming\\Caddy\\autosave.json"}
2025/04/02 18:04:33.887 ←[34mINFO←[0m   serving initial configuration
2025/04/02 18:04:33.888 ←[34mINFO←[0m   tls     storage cleaning happened too recently; skipping for now        {"storage": "FileStorage:C:\\Users\\username\\AppData\\Roaming\\Caddy", "instance": "49e59ef7-a3f2-4fd3-a466-4f510b2ddf53", "try_again": "2025/04/03 18:04:33.888", "try_again_in": 86400}
2025/04/02 18:04:33.888 ←[34mINFO←[0m   tls     finished cleaning storage units
2025/04/02 18:05:40.358 ←[35mDEBUG←[0m  events  event   {"name": "tls_get_certificate", "id": "83c27146-a56d-4594-8c1e-7c2d6d7e247f", "origin": "tls", "data": {"client_hello":{"CipherSuites":[49196,49195,49200,49199,159,158,49188,49187,49192,49191,49162,49161,49172,49171,157,156,61,60,53,47,10],"ServerName":"localhost","SupportedCurves":[29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[2052,2053,2054,1025,1281,513,1027,1283,515,514,1537,1539],"SupportedProtos":["http/1.1"],"SupportedVersions":[771,770,769],"RemoteAddr":{"IP":"::1","Port":58411,"Zone":""},"LocalAddr":{"IP":"::1","Port":8080,"Zone":""}}}}
2025/04/02 18:05:40.358 ←[35mDEBUG←[0m  tls.handshake   choosing certificate    {"identifier": "localhost", "num_choices": 1}
2025/04/02 18:05:40.359 ←[35mDEBUG←[0m  tls.handshake   default certificate selection results   {"identifier": "localhost", "subjects": ["localhost"], "managed": true, "issuer_key": "local", "hash": "edc650750ad0172995bdc5bd6741ebb0360839308c679698931e56f91d0283bb"}
2025/04/02 18:05:40.359 ←[35mDEBUG←[0m  tls.handshake   matched certificate in cache    {"remote_ip": "::1", "remote_port": "58411", "subjects": ["localhost"], "managed": true, "expiration": "2025/04/02 23:25:40.000", "hash": "edc650750ad0172995bdc5bd6741ebb0360839308c679698931e56f91d0283bb"}
2025/04/02 18:05:40.365 ←[34mINFO←[0m   http.authentication.providers.jwt       user authenticated      {"token_string": "eyJhbGciOiJIUzI1…mnOXCO8tvYAI4eq0", "user_claim": "name", "id": "hola"}
2025/04/02 18:05:40.365 ←[35mDEBUG←[0m  http.handlers.reverse_proxy     selected upstream       {"dial": "localhost:8090", "total_upstreams": 1}
2025/04/02 18:05:40.378 ←[35mDEBUG←[0m  http.handlers.reverse_proxy     upstream roundtrip      {"upstream": "localhost:8090", "duration": 0.0125187, "request": {"remote_ip": "::1", "remote_port": "58411", "client_ip": "::1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost", "uri": "/ws", "headers": {"X-Forwarded-Host": ["localhost:8080"], "Authorization": ["REDACTED"], "X-Forwarded-For": ["::1"], "Sec-Websocket-Key": ["{>Sec-WebSocket-Key}"], "Sec-Websocket-Version": ["13"], "Upgrade": ["websocket"], "X-Real-Ip": ["::1"], "Connection": ["Upgrade"], "X-Forwarded-Proto": ["https"], "User-Agent": ["curl/8.9.1"], "Accept": ["*/*"], "X-Forwarded-Port": ["8080"]}, "tls": {"resumed": false, "version": 771, "cipher_suite": 49195, "proto": "http/1.1", "server_name": "localhost"}}, "headers": {"Date": ["Wed, 02 Apr 2025 18:05:40 GMT"], "Upgrade": ["websocket"], "Sec-Websocket-Accept": ["8uqguKSXBj9pNIoSSdV7K8mDnXw="], "Connection": ["Upgrade"], "Server": ["Jetty(12.0.16)"]}, "status": 101}
2025/04/02 18:05:40.378 ←[35mDEBUG←[0m  http.handlers.reverse_proxy     upgrading connection    {"upstream": "localhost:8090", "duration": 0.0125187, "request": {"remote_ip": "::1", "remote_port": "58411", "client_ip": "::1", "proto": "HTTP/1.1", "method": "GET", "host": "localhost", "uri": "/ws", "headers": {"X-Forwarded-Host": ["localhost:8080"], "Authorization": ["REDACTED"], "X-Forwarded-For": ["::1"], "Sec-Websocket-Key": ["{>Sec-WebSocket-Key}"], "Sec-Websocket-Version": ["13"], "Upgrade": ["websocket"], "X-Real-Ip": ["::1"], "Connection": ["Upgrade"], "X-Forwarded-Proto": ["https"], "User-Agent": ["curl/8.9.1"], "Accept": ["*/*"], "X-Forwarded-Port": ["8080"]}, "tls": {"resumed": false, "version": 771, "cipher_suite": 49195, "proto": "http/1.1", "server_name": "localhost"}}, "http_version": 1}

This header is passed as is "Sec-Websocket-Key": ["{>Sec-WebSocket-Key}"] [Im unsure if this behaviour is expected or not]

Same in jetty logs as well

23:35:40.373 [qtp1147580192-41] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:IN_VALUE --> FIELD(Sec-WebSocket-Key: {>Sec-WebSocket-Key})
23:35:40.373 [qtp1147580192-41] DEBUG o.eclipse.jetty.http.HttpParser - parsedHeader(null) header=Sec-WebSocket-Key, headerString=[Sec-WebSocket-Key], valueString=[{>Sec-WebSocket-Key}]
23:35:40.373 [qtp1147580192-41] DEBUG o.eclipse.jetty.http.HttpParser - HEADER:FIELD --> VALUE

3. Caddy version:

caddy.exe --version
v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=

4. How I installed and ran Caddy:

a. System environment:

Build with xcaddy and running on Windows 10. Running from the same directory using cmd in blocking mode and not as a service. Caddy is build using xcaddy on the module GitHub - ggicci/caddy-jwt: 🆔 Caddy Module JWT Authentication

5. Links to relevant resources:

This header Sec-WebSocket-Key seem to be important because it might have been mandated by jetty

Goal

Please help so that I dont have to hardcode the value to caddy. Please also let me know if this can be generated by caddy itself ?

As of now, I ended up doing this to use a unique ID

header_up Sec-WebSocket-Key {http.request.uuid}

This does work however Im willing to listen what you have to say on this.

Thank you

Remove all of these. Caddy does the right thing already. See the “Defaults” section in the reverse_proxy docs.

This is not the syntax for placeholders. Review the “Placeholders” section on the “Caddyfile Concepts” page.

What is the source of the Sec-WebSocket-Key? If it comes from the client headers, then it will be passed by default without extra config. If it is from elsewhere, you’ll need to use the respective placeholder.

This header Sec-WebSocket-Key seem to be important because it might have been mandated by jetty

Thank you for the other suggestion on placeholders! So it seems that jetty is expecting this header with the websocket request. I have managed to kind of have a workaround by using the {http.request.uuid} .