Skip to content

feat: honour Raw-Request-URI as the HTTP/2 :path - #1280

Open
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:http2-raw-request-uri
Open

feat: honour Raw-Request-URI as the HTTP/2 :path#1280
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:http2-raw-request-uri

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

Refs #1273.

That issue asks for a way to send a request whose path renders exactly as provided, because Uri cannot round-trip a percent-encoded path — it decodes segments when parsing and re-encodes them with the pchar-base keep-set, which leaves sub-delims raw, so %2B renders back as +. AWS SigV4 signs the encoded path, so an S3 object key containing + or = is rejected with SignatureDoesNotMatch after the round trip.

While investigating I found the issue's premise is only half right. The escape hatch it proposes (option 2, "a pre-rendered request target that the client renderer honors") already exists for HTTP/1.1: HttpRequestRendererFactory special-cases a Raw-Request-URI header and emits it verbatim as the request target. It is a SyntheticHeader, so it is not also rendered as a header line; it is covered by tests in RequestRendererSpec and documented under "synthetic headers" in http-model.md, whose example is literally an S3 path.

What is genuinely missing is HTTP/2. RequestRendering.initialHeaderPairs built :path from request.uri.toHttpRequestTargetOriginForm unconditionally and ignored the header, so the same HttpRequest produced a different request target depending on which protocol was negotiated.

Modification

Take Raw-Request-URI into account when building :path, mirroring HTTP/1.1.

The header needs no new suppression logic: being a SyntheticHeader it was already excluded from the rendered header block by the existing renderInRequests filter, so it was silently dropped rather than mis-sent. As in HTTP/1.1, the value is used exactly as given — supplying a valid origin-form target is the caller's responsibility, and this is stated in the docs.

Also documents, in the existing synthetic-headers section of http-model.md, that the header is honoured by both clients, why it is needed (the Uri round-trip loss), and the SigV4 case.

Result

aws-spi-pekko-http, and any proxy or pass-through use case that must not alter the request target, can pass SdkHttpRequest.encodedPath() through unchanged over HTTP/2 as well as HTTP/1.1.

This does not change the Uri model, so the round-trip limitation described in #1273 remains as such; it makes the existing escape hatch work uniformly across protocols. I'd suggest #1273 stays open if a raw Uri.Path representation is still wanted — that is a much larger change, since Path is a sealed ADT matched exhaustively across the codebase and in user code.

Tests

Two cases added to Http2ClientSpec:

  • a request carrying Raw-Request-URI("/a%2Bb%20c") sends that value as :path. The assertion compares the whole pseudo/header set, so it also pins that the header itself is not rendered. Verified this fails on main:path comes out as /a+b%20c — and passes with the change.
  • without the header, the path is still re-encoded from the Uri. This one passes both before and after; it is a characterization test that pins the round-trip loss (/a%2Bb%20c in, /a+b%20c out) which motivates the escape hatch.

sbt "http2-tests/testOnly *Http2ClientSpec *Http2ClientServerSpec *Http2PersistentClientSpec *Http2ServerSpec" — 183 pass. sbt http-core/mimaReportBinaryIssues — clean (the change is confined to an @InternalApi private[http2] class). Native scalafmt clean.

References

Refs #1273.

🤖 Generated with Claude Code

The HTTP/1.1 request renderer lets a caller supply the request target
verbatim through a `Raw-Request-URI` header, bypassing `Uri` rendering. The
HTTP/2 renderer built `:path` from `request.uri` unconditionally, so the
same request produced a different target depending on the protocol
negotiated.

Take the header into account when building `:path`, as HTTP/1.1 does. The
header is a `SyntheticHeader`, so it was already excluded from the rendered
header block by the `renderInRequests` filter and is only consumed here. As
in HTTP/1.1 the value is used as given -- supplying a valid origin-form
target is the caller's responsibility.

This matters because `Uri` cannot round-trip a percent-encoded path: it
decodes segments when parsing and re-encodes them with a keep-set that
leaves sub-delims raw, so `%2B` renders back as `+`. Callers that must
reproduce a target byte-for-byte -- AWS SigV4 signs the encoded path, so an
S3 key containing `+` or `=` fails with SignatureDoesNotMatch otherwise --
had no way to do so over HTTP/2.

Refs apache#1273.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant