Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/main/paradox/common/http-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ Scala
Java
: @@snip [ModelDocTest.java](/docs/src/test/java/docs/http/javadsl/ModelDocTest.java) { #synthetic-header-s3 }

The `Raw-Request-URI` header is honoured by both the HTTP/1.1 and the HTTP/2 client; over HTTP/2 its value is sent as
the `:path` pseudo-header. It is consumed by the request engine and never rendered as a header of its own, and its
value is used exactly as given — it is the caller's responsibility to supply a valid request target.

This is the supported way to send a request target that @apidoc[Uri] cannot reproduce on its own. `Uri` percent-decodes
path segments when parsing and re-encodes them with a keep-set that leaves sub-delims raw, so an encoded *pchar* does
not survive the round trip — `%2B` is rendered back as `+`, for instance. Callers that must reproduce the target
byte-for-byte should pass it through this header. AWS SigV4 is a typical case: the signature covers the encoded path,
so an S3 object key containing `+` or `=` must reach the wire exactly as it was signed.

## HttpResponse

An @apidoc[HttpResponse] consists of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import pekko.event.LoggingAdapter
import pekko.http.impl.engine.http2.FrameEvent.ParsedHeadersFrame
import pekko.http.impl.engine.rendering.DateHeaderRendering
import pekko.http.scaladsl.model._
import pekko.http.scaladsl.model.headers.`Raw-Request-URI`
import pekko.http.scaladsl.settings.ClientConnectionSettings
import pekko.http.scaladsl.settings.ServerSettings
import pekko.util.OptionVal
Expand Down Expand Up @@ -73,10 +74,21 @@ private[http2] class RequestRendering(
headerPairs += ":method" -> request.method.value
headerPairs += ":scheme" -> request.uri.scheme
headerPairs += ":authority" -> request.uri.authority.toString
headerPairs += ":path" -> request.uri.toHttpRequestTargetOriginForm.toString
// a `Raw-Request-URI` header supplies the request target verbatim, as it does in the HTTP/1.1 renderer. `Uri`
// percent-decodes path segments when parsing and re-encodes them with a keep-set that leaves sub-delims raw, so
// it cannot round-trip an encoded pchar (`%2B` renders as `+`). Callers that must reproduce the target
// byte-for-byte -- AWS SigV4 signs the encoded path, for instance -- pass it through this header.
headerPairs += ":path" -> rawRequestTarget(request).getOrElse(
request.uri.toHttpRequestTargetOriginForm.toString)
headerPairs
}

// `Raw-Request-URI` is a SyntheticHeader, so it is already excluded from the rendered header block by the
// `renderInRequests` filter and is only consumed here. As in HTTP/1.1, the value is taken as given: it is the
// caller's responsibility that it is a valid origin-form target.
private def rawRequestTarget(request: HttpRequest): Option[String] =
request.headers.collectFirst { case `Raw-Request-URI`(rawUri) => rawUri }

override lazy val peerIdHeader: Option[(String, String)] =
settings.userAgentHeader.map(h => h.lowercaseName -> h.value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import pekko.http.scaladsl.model.headers.{
`Cache-Control`,
`Content-Length`,
`Content-Type`,
`Raw-Request-URI`,
RawHeader
}
import pekko.http.scaladsl.model.headers.CacheDirectives._
Expand Down Expand Up @@ -128,6 +129,41 @@ class Http2ClientSpec extends PekkoSpecWithMaterializer("""
expectedResponse = HPackSpecExamples.FirstResponse)
})

"send the Raw-Request-URI header verbatim as :path".inAssertAllStagesStopped(
new SimpleRequestResponseRoundtripSetup {
requestResponseRoundtrip(
streamId = 1,
// the `uri` is deliberately the lossy round-trip of the raw target, to show the header wins
request = HttpRequest(
uri = "https://www.example.com/a+b%20c",
headers = List(`Raw-Request-URI`("/a%2Bb%20c"))),
expectedHeaders = defaultExpectedHeaders.map {
case (":path", _) => ":path" -> "/a%2Bb%20c"
case other => other
},
response = Seq(
HeadersFrame(streamId = 1, endStream = true, endHeaders = true,
HPackSpecExamples.C61FirstResponseWithHuffman, None)),
expectedResponse = HPackSpecExamples.FirstResponse)
})

"re-encode the path from the Uri when no Raw-Request-URI header is present".inAssertAllStagesStopped(
new SimpleRequestResponseRoundtripSetup {
requestResponseRoundtrip(
streamId = 1,
request = HttpRequest(uri = "https://www.example.com/a%2Bb%20c"),
// `Uri` decodes `%2B` when parsing and renders `+` back, since `+` is kept raw by the pchar keep-set.
// This is the round-trip loss that makes the `Raw-Request-URI` escape hatch necessary.
expectedHeaders = defaultExpectedHeaders.map {
case (":path", _) => ":path" -> "/a+b%20c"
case other => other
},
response = Seq(
HeadersFrame(streamId = 1, endStream = true, endHeaders = true,
HPackSpecExamples.C61FirstResponseWithHuffman, None)),
expectedResponse = HPackSpecExamples.FirstResponse)
})

"GOAWAY when the response has an invalid headers frame".inAssertAllStagesStopped(new TestSetup with NetProbes {
val streamId = 0x1
user.emitRequest(HttpRequest(uri = "http://www.example.com/"))
Expand Down