You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue is just to gather feedback if people really need that.
Play should support applications published below a request-specific public path prefix that a trusted reverse proxy removes before forwarding the request to Play.
The eventual X-Forwarded-Prefix support should be built on a general public request pathBase model. It should not rewrite the backend request target or reuse play.http.context as a substitute for dynamic request metadata.
Motivation
Consider a public request to:
https://example.com/store/products/42
The reverse proxy mounts the Play application below /store, strips that prefix, and forwards:
GET /products/42 HTTP/1.1Host: play.internalX-Forwarded-Prefix: /store
Play must continue routing /products/42, while request-aware links and redirects intended for the client need to use /store/products/42.
play.http.context does not model this correctly. It is a static application and routing context that is expected to be present in the path received by Play. A forwarded prefix is request-specific public metadata for a path component that has already been removed upstream.
Proposed Request Model
Introduce a general path-base property before interpreting the proxy header:
The name and necessity of publicPath remain an API design decision; pathBase is the essential model.
The following values must retain backend semantics:
request.path
request.uri
request.target
router matching
pathBase must be carried separately and preserved through Scala and Java request implementations, wrappers, builders, fake requests, copy methods, server conversions, and error requests.
It should not be stored by mutating RequestTarget, because that represents the target received by Play after proxy processing.
Suggested Delivery
This should preferably be delivered as two reviewable pull requests.
1. Introduce RequestHeader.pathBase
The first PR should add the general request metadata independently of any specific proxy header.
It should:
Default pathBase to the empty string.
Define and enforce normalization and validation rules.
Preserve backend path, URI, target, and routing behavior.
Add Scala and Java APIs and builders.
Define safe composition of pathBase with a backend path.
Integrate request-aware URL generation and redirects.
Document the distinction from play.http.context.
Request-aware APIs that should be audited include:
Scala Call.absoluteURL and Call.webSocketURL.
Java Call.absoluteURL(request) and Call.webSocketURL(request).
Call.relative and Java relativeTo.
HTTPS redirects and helpers that reconstruct the current public URL.
Equivalent Scala and Java APIs.
Cookie-path behavior and any cache keys derived from request paths.
Plain generated Call.url has no RequestHeader, so a dynamic path base cannot be applied implicitly. It should remain compatible unless a clear request-aware API is introduced. The PR must document this boundary rather than making only some route URLs change invisibly.
2. Map trusted X-Forwarded-Prefix to pathBase
The second PR should add an explicit opt-in, for example:
play.http.forwarded.trustXForwardedPrefix = false
The default must remain false.
Suggested behavior:
Apply only to Play's x-forwarded forwarding mode. RFC 7239 has no prefix parameter.
Require the directly connected peer to be a configured trusted proxy.
Accept exactly one authoritative, non-empty, valid value.
Work without X-Forwarded-For; the direct trusted peer is authoritative for this independent metadata.
Reject or ignore repeated fields, comma-separated lists, malformed values, and ambiguous input according to a documented fail-closed policy.
Populate only request.pathBase.
Never modify request.path, request.uri, request.target, or routing.
Keep the opt-in independent from forwarded proto, host, and port options.
Leave the raw header available through request.headers, consistently with Play's other forwarded metadata.
The implementation must reuse Play's direct-peer trust boundary and strict single-value handling patterns rather than selecting an arbitrary first value from an appended list.
Validation And Normalization
The path-base PR should settle these cases before the header parser is added:
Empty path base.
/ and whether it canonicalizes to the empty base.
Required leading slash.
Trailing slash normalization.
Repeated slashes and dot segments.
Percent-encoded bytes, including encoded separators.
Query, fragment, backslash, control-character, and invalid UTF-8 input.
Composition with a backend root path.
Composition with a non-root play.http.context.
Non-path request targets such as * and authority-form CONNECT.
Avoid decoding and re-encoding a valid prefix in a way that changes its public URL meaning. Use structured URI/path parsing where practical rather than ad hoc string replacement.
Security
Trusting X-Forwarded-Prefix is security-sensitive. Documentation must state
that:
Untrusted clients must not be able to connect directly to the Play server.
The trusted edge proxy must remove or overwrite any client-supplied X-Forwarded-Prefix value.
A Play trust option cannot repair a deployment where the proxy appends to an attacker-controlled value.
The value can affect generated links, redirects, cookie scope, cache keys, and externally visible URLs.
Symfony's CVE-2021-41267 demonstrates the cache-poisoning risk of trusting unsanitized X-Forwarded-Prefix. The initial implementation should therefore prefer one sanitized value from the direct trusted proxy over multi-hop or first-value heuristics.
Compatibility
Existing applications must keep the empty path base by default.
X-Forwarded-Prefix must have no effect until explicitly enabled.
Existing backend routing and play.http.context behavior must remain unchanged.
Existing Call.url output should not change merely because a request has a dynamic path base unless a new request-aware API explicitly requests that behavior.
Test Coverage
The path-base API should cover:
Empty and non-empty path bases in Scala and Java.
Builders, fake requests, wrappers, conversions, copies, and error requests.
Backend path, URI, target, and routing remaining unchanged.
Public-path composition.
Absolute HTTP and WebSocket URLs.
Relative URLs and redirects.
Interaction with play.http.context.
The chosen normalization and invalid-value behavior.
The header integration should additionally cover:
Default-disabled behavior.
One valid value from a trusted direct proxy.
Operation without X-Forwarded-For.
An untrusted direct peer.
Repeated physical fields and comma-separated values.
Empty, malformed, and ambiguous values.
Independent composition with trusted forwarded scheme and authority.
Netty and Pekko HTTP integration.
Explicit regression coverage for header injection and public URL/cache
poisoning scenarios.
Non-Goals For The Initial Implementation
Do not resurrect the old design that requires the value to equal play.http.context.
Do not prepend the prefix to the backend request target or router input.
Do not implement multi-hop or appended-prefix selection without a separate trusted-hop design.
Do not add an RFC 7239 extension parameter and call it standardized prefix support.
Do not automatically rewrite application cookie paths without an explicit, documented API decision.
References
RFC 7239, which defines for, by, host, and proto, but not a path prefix.
Note
This issue is just to gather feedback if people really need that.
Play should support applications published below a request-specific public path prefix that a trusted reverse proxy removes before forwarding the request to Play.
The eventual
X-Forwarded-Prefixsupport should be built on a general public requestpathBasemodel. It should not rewrite the backend request target or reuseplay.http.contextas a substitute for dynamic request metadata.Motivation
Consider a public request to:
The reverse proxy mounts the Play application below
/store, strips that prefix, and forwards:Play must continue routing
/products/42, while request-aware links and redirects intended for the client need to use/store/products/42.play.http.contextdoes not model this correctly. It is a static application and routing context that is expected to be present in the path received by Play. A forwarded prefix is request-specific public metadata for a path component that has already been removed upstream.Proposed Request Model
Introduce a general path-base property before interpreting the proxy header:
with a Java equivalent:
A derived public path may also be useful:
request.publicPath // "/store/products/42"The name and necessity of
publicPathremain an API design decision;pathBaseis the essential model.The following values must retain backend semantics:
request.pathrequest.urirequest.targetpathBasemust be carried separately and preserved through Scala and Java request implementations, wrappers, builders, fake requests, copy methods, server conversions, and error requests.It should not be stored by mutating
RequestTarget, because that represents the target received by Play after proxy processing.Suggested Delivery
This should preferably be delivered as two reviewable pull requests.
1. Introduce
RequestHeader.pathBaseThe first PR should add the general request metadata independently of any specific proxy header.
It should:
pathBaseto the empty string.pathBasewith a backend path.play.http.context.Request-aware APIs that should be audited include:
Call.absoluteURLandCall.webSocketURL.Call.absoluteURL(request)andCall.webSocketURL(request).Call.relativeand JavarelativeTo.Plain generated
Call.urlhas noRequestHeader, so a dynamic path base cannot be applied implicitly. It should remain compatible unless a clear request-aware API is introduced. The PR must document this boundary rather than making only some route URLs change invisibly.2. Map trusted
X-Forwarded-PrefixtopathBaseThe second PR should add an explicit opt-in, for example:
The default must remain
false.Suggested behavior:
x-forwardedforwarding mode. RFC 7239 has no prefix parameter.X-Forwarded-For; the direct trusted peer is authoritative for this independent metadata.request.pathBase.request.path,request.uri,request.target, or routing.request.headers, consistently with Play's other forwarded metadata.The implementation must reuse Play's direct-peer trust boundary and strict single-value handling patterns rather than selecting an arbitrary first value from an appended list.
Validation And Normalization
The path-base PR should settle these cases before the header parser is added:
/and whether it canonicalizes to the empty base.play.http.context.*and authority-formCONNECT.Avoid decoding and re-encoding a valid prefix in a way that changes its public URL meaning. Use structured URI/path parsing where practical rather than ad hoc string replacement.
Security
Trusting
X-Forwarded-Prefixis security-sensitive. Documentation must statethat:
X-Forwarded-Prefixvalue.Symfony's CVE-2021-41267 demonstrates the cache-poisoning risk of trusting unsanitized
X-Forwarded-Prefix. The initial implementation should therefore prefer one sanitized value from the direct trusted proxy over multi-hop or first-value heuristics.Compatibility
X-Forwarded-Prefixmust have no effect until explicitly enabled.play.http.contextbehavior must remain unchanged.Call.urloutput should not change merely because a request has a dynamic path base unless a new request-aware API explicitly requests that behavior.Test Coverage
The path-base API should cover:
play.http.context.The header integration should additionally cover:
X-Forwarded-For.poisoning scenarios.
Non-Goals For The Initial Implementation
play.http.context.References
for,by,host, andproto, but not a path prefix.ProxyFix, includingx_prefixPathBaseAcceptance Criteria
X-Forwarded-Prefixcan populate it withoutX-Forwarded-For.