This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/main by this push:
new 2f88aaede fix: do not send a response body to HTTP/2 HEAD requests
(#1239)
2f88aaede is described below
commit 2f88aaedecd70570a79fabcb6a5092ccc3f83df0
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Sep 1 11:35:47 2026 +0100
fix: do not send a response body to HTTP/2 HEAD requests (#1239)
* fix: do not send a response body to HTTP/2 HEAD requests
Motivation:
The HTTP/2 engine had no notion of the HEAD method, which #1238 pinned as
three
failing expectations:
- the response entity was emitted as DATA frames, although RFC 9110 section
9.3.2 says a server MUST NOT send content in a response to a HEAD request
- `transparent-head-requests` was applied only by HttpServerBluePrint, so
it had
no effect over HTTP/2
- a 304 (and a 204) got `content-length: 0`, where HTTP/1.1 omits the
header;
RFC 9110 section 15.4.5 expects a 304 to carry the content-length a 200
would
have had, so a zero is actively misleading
The response path only ever sees an HttpResponse plus its stream id, so
ResponseRendering cannot know the request method. Carrying it on an
attribute
would only work for the `bind` API, since `bindFlow` users copy the stream
id
attribute by hand.
Modification:
Track the method where it is already known per connection.
Http2StreamHandling
records the stream ids of incoming HEAD requests when the request HEADERS
frame
opens the stream, and handleOutgoingCreated cancels the response data and
sends
the initial headers with endStream set for those streams. The header pairs
are
left untouched, so the peer still learns the content-length it would have
got
for a GET. Entries are removed when the response is created and when the
stream
closes.
Because that tracking reads the method off the wire below the HTTP layer,
it is
unaffected by RequestParsing rewriting HEAD to GET, so
transparent-head-requests
can now be honoured for HTTP/2 the same way HttpServerBluePrint honours it
for
HTTP/1.1.
ResponseRendering gains the status based part of the rules HTTP/1.1 applies
via
HttpMethod.contentLengthAllowed, so 1xx, 204 and 304 no longer render a
content-length.
Result:
A HEAD request over HTTP/2 gets headers only, with the content-length the
resource would have had, and transparent-head-requests behaves as it does
for
HTTP/1.1.
Tests:
- sbt "http2-tests / test" - 349 passed, 25 ignored, 26 pending
- sbt +mimaReportBinaryIssues - success
- scalafmt --mode diff-ref=upstream/main - clean
- git diff --check - clean
References:
Refs #1236, Refs #1238
* test: cover the status based content-length rules and CONNECT over HTTP/2
Motivation:
PR #962 introduced two sets of rules in HttpMethod.contentLengthAllowed: a
method specific one for HEAD and CONNECT, and a status based one that
applies to
every method. HTTP/2 had no coverage for either. The 204 and 304 cases added
alongside the HEAD fixes were exercised with a HEAD request only, even
though
the status based rules do not depend on the method, so a regression on GET
would
have gone unnoticed.
Modification:
Move the 204 and 304 cases out of the HEAD section into a new section that
drives them with a plain GET, and add the 200 and 205 cases. 205 is the
status
PR #962 was really aimed at: RFC 9112 section 6.3 exempts only 1xx, 204 and
304
from framing, so a 205 must be framed, and it is what http4s/http4s#7919
reports against another server.
Add a RequestParsingSpec case pinning that a CONNECT request is rejected,
because neither RFC 9113 section 8.5 CONNECT nor RFC 8441 extended CONNECT
is
supported: the request omits ":scheme" and ":path", which the parser treats
as
mandatory. That makes it explicit that the CONNECT specific rule in
HttpMethods.contentLengthAllowed is unreachable over HTTP/2.
Result:
The status based rules are covered independently of the request method, and
the
absence of CONNECT support is recorded rather than assumed.
Tests:
- sbt "http2-tests / test" - 352 passed, 25 ignored, 26 pending
- scalafmt --mode diff-ref=upstream/main - clean
- git diff --check - clean
References:
Refs #1236, Refs #962
---
.../http/impl/engine/http2/Http2Blueprint.scala | 13 +++
.../impl/engine/http2/Http2StreamHandling.scala | 22 ++++-
.../impl/engine/http2/HttpMessageRendering.scala | 21 ++++-
.../http/impl/engine/http2/RequestParsing.scala | 7 +-
.../http/impl/engine/http2/Http2ServerSpec.scala | 104 ++++++++++++++-------
.../impl/engine/http2/RequestParsingSpec.scala | 11 +++
6 files changed, 139 insertions(+), 39 deletions(-)
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
index aa7de7bda..0907bcf6a 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2Blueprint.scala
@@ -63,6 +63,19 @@ private[http2] case class Http2SubStream(
def withCorrelationAttributes(newAttributes: Map[AttributeKey[?], ?]):
Http2SubStream =
copy(correlationAttributes = newAttributes)
+ /**
+ * Returns a copy that carries no data and ends the stream with its initial
headers. The header pairs are kept as
+ * they are, so that a response keeps advertising the `content-length` the
peer would have received otherwise.
+ *
+ * Used for responses to HEAD requests, which must not carry content (RFC
9110 section 9.3.2). The caller is
+ * responsible for cancelling `data` if it is a stream.
+ */
+ def withoutData: Http2SubStream =
+ copy(
+ initialHeaders = initialHeaders.copy(endStream = true),
+ trailingHeaders = OptionVal.None,
+ data = Left(ByteString.empty))
+
/**
* Create the request entity (when we're the server) or response entity
(when we're the client) for this substream
*/
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2StreamHandling.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2StreamHandling.scala
index e74691ab4..5de3aa173 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2StreamHandling.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/Http2StreamHandling.scala
@@ -18,7 +18,7 @@ import pekko.annotation.InternalApi
import pekko.http.impl.engine.http2.FrameEvent._
import pekko.http.impl.engine.http2.Http2Protocol.ErrorCode
import pekko.http.impl.engine.rendering.DateHeaderRendering
-import pekko.http.scaladsl.model.{ AttributeKey, HttpEntity }
+import pekko.http.scaladsl.model.{ AttributeKey, HttpEntity, HttpMethods }
import pekko.http.scaladsl.model.http2.PeerClosedStreamException
import pekko.http.scaladsl.settings.Http2CommonSettings
import pekko.macros.LogHelper
@@ -54,6 +54,9 @@ private[http2] trait Http2StreamHandling extends
GraphStageLogic with LogHelper
def flowController: IncomingFlowController =
IncomingFlowController.default(settings)
+ private def isHeadRequest(frame: ParsedHeadersFrame): Boolean =
+ frame.keyValuePairs.exists { case (name, value) => name == ":method" &&
(value eq HttpMethods.HEAD) }
+
/**
* Tries to generate demand of SubStreams on the inlet from the user
handler. The
* attemp to demand will succeed if the inlet is open and has no pending
pull, and,
@@ -65,6 +68,10 @@ private[http2] trait Http2StreamHandling extends
GraphStageLogic with LogHelper
def tryPullSubStreams(): Unit
private val streamStates = new
mutable.LongMap[StreamState](settings.maxConcurrentStreams)
+ // Stream ids of incoming HEAD requests. The response to a HEAD request must
not carry content
+ // (RFC 9110 section 9.3.2), but the response path only ever sees an
HttpResponse and so cannot know the request
+ // method. Entries are removed when the response is created or when the
stream is closed.
+ private val headRequestStreamIds = mutable.Set.empty[Int]
private var largestIncomingStreamId = 0
private var outstandingConnectionLevelWindow =
Http2Protocol.InitialWindowSize
private var totalBufferedData = 0
@@ -118,7 +125,15 @@ private[http2] trait Http2StreamHandling extends
GraphStageLogic with LogHelper
updateState(e.streamId, _.handle(e), "handleStreamEvent", e.frameTypeName)
/** Called by Http2ServerDemux when a stream comes in from the user-handler
*/
- def handleOutgoingCreated(stream: Http2SubStream): Unit = {
+ def handleOutgoingCreated(outgoing: Http2SubStream): Unit = {
+ // a response to a HEAD request must not carry content (RFC 9110 section
9.3.2); the headers are sent unchanged
+ // so that the peer still learns the `content-length` it would have
received for a GET
+ val stream =
+ if (headRequestStreamIds.remove(outgoing.streamId) &&
outgoing.hasEntity) {
+ outgoing.data.foreach(_.runWith(Sink.cancelled)(subFusingMaterializer))
+ outgoing.withoutData
+ } else outgoing
+
stream.initialHeaders.priorityInfo.foreach(multiplexer.updatePriority)
if (streamFor(stream.streamId) != Closed) {
multiplexer.pushControlFrame(stream.initialHeaders)
@@ -188,6 +203,7 @@ private[http2] trait Http2StreamHandling extends
GraphStageLogic with LogHelper
newState match {
case Closed =>
streamStates.remove(streamId)
+ headRequestStreamIds -= streamId
if (streamStates.isEmpty) onAllStreamsClosed()
tryPullSubStreams()
case newState => streamStates.put(streamId, newState)
@@ -300,6 +316,7 @@ private[http2] trait Http2StreamHandling extends
GraphStageLogic with LogHelper
correlationAttributes: Map[AttributeKey[?], ?] = Map.empty):
StreamState =
event match {
case frame @ ParsedHeadersFrame(streamId, endStream, _, _, _) =>
+ if (isServer && isHeadRequest(frame)) headRequestStreamIds +=
streamId
if (endStream) {
dispatchSubstream(frame, Left(ByteString.empty),
correlationAttributes)
nextStateEmpty
@@ -621,6 +638,7 @@ private[http2] trait Http2StreamHandling extends
GraphStageLogic with LogHelper
multiplexer.pushControlFrame(RstStreamFrame(streamId, ErrorCode.CANCEL))
// FIXME: go through state machine and don't manipulate vars directly
here
streamStates.remove(streamId)
+ headRequestStreamIds -= streamId
wasClosed = true
buffer = ByteString.empty
trailingHeaders = None
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRendering.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRendering.scala
index d2dc3028e..5e301267e 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRendering.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/HttpMessageRendering.scala
@@ -51,6 +51,13 @@ private[http2] class ResponseRendering(settings:
ServerSettings, val log: Loggin
override lazy val peerIdHeader: Option[(String, String)] =
settings.serverHeader.map(h => h.lowercaseName -> h.value)
+ // Mirrors the status based part of the rules the HTTP/1.1 renderer applies
through
+ // HttpMethod.contentLengthAllowed: no content-length for 1xx, 204 or 304
(RFC 9110 sections 8.6 and 15.4.5, where a
+ // 304 is supposed to carry the content-length a 200 would have had rather
than a made up zero). GET is used here
+ // only because it carries the rules that are common to all methods; the
request method is not known on this path.
+ protected override def contentLengthAllowed(response: HttpResponse): Boolean
=
+ HttpMethods.GET.contentLengthAllowed(response.status)
+
}
/** INTERNAL API */
@@ -86,10 +93,16 @@ private[http2] sealed abstract class MessageRendering[R <:
HttpMessage] extends
protected def peerIdHeader: Option[(String, String)]
protected def dateHeaderRendering: DateHeaderRendering
+ /**
+ * Whether a `content-length` may be rendered for this message. Always true
for requests, where the length simply
+ * describes the request body.
+ */
+ protected def contentLengthAllowed(r: R): Boolean = true
+
def apply(r: R): Http2SubStream = {
val headerPairs = initialHeaderPairs(r)
- HttpMessageRendering.addContentHeaders(headerPairs, r.entity)
+ HttpMessageRendering.addContentHeaders(headerPairs, r.entity,
contentLengthAllowed(r))
HttpMessageRendering.renderHeaders(r.headers, headerPairs, peerIdHeader,
log, isServer = r.isResponse,
shouldRenderAutoHeaders = true, dateHeaderRendering)
@@ -114,10 +127,12 @@ private[http2] object HttpMessageRendering {
/**
* Mutates `headerPairs` adding headers related to content (type and length).
*/
- def addContentHeaders(headerPairs: VectorBuilder[(String, String)], entity:
HttpEntity): Unit = {
+ def addContentHeaders(headerPairs: VectorBuilder[(String, String)], entity:
HttpEntity,
+ contentLengthAllowed: Boolean): Unit = {
if (entity.contentType ne ContentTypes.NoContentType)
headerPairs += "content-type" -> entity.contentType.toString
- entity.contentLengthOption.foreach(headerPairs += "content-length" ->
_.toString)
+ if (contentLengthAllowed)
+ entity.contentLengthOption.foreach(headerPairs += "content-length" ->
_.toString)
}
def renderHeaders(
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala
index 67e998a21..386873947 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala
@@ -97,12 +97,17 @@ private[http2] object RequestParsing {
val entity = subStream.createEntity(contentLength, contentType)
+ // mirrors HttpServerBluePrint's ControllerStage for HTTP/1.1: the
handler sees a GET, while the substream
+ // handling in Http2StreamHandling still knows the request was a HEAD
and strips the response body
+ val effectiveMethod =
+ if (method == HttpMethods.HEAD &&
serverSettings.transparentHeadRequests) HttpMethods.GET else method
+
val (path, rawQueryString) = pathAndRawQuery
val authorityOrDefault: Uri.Authority = if (authority == null)
Uri.Authority.Empty else authority
val uri = Uri(scheme, authorityOrDefault, path, rawQueryString)
val attributes = baseAttributes.updated(Http2.streamId,
subStream.streamId)
- new HttpRequest(method, uri, headers.result(), attributes, entity,
HttpProtocols.`HTTP/2.0`)
+ new HttpRequest(effectiveMethod, uri, headers.result(), attributes,
entity, HttpProtocols.`HTTP/2.0`)
}
@tailrec
diff --git
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
index d403c0e44..8183220fa 100644
---
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
+++
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
@@ -411,16 +411,32 @@ class Http2ServerSpec extends
Http2SpecWithMaterializer("""
network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap should contain(":status" -> "200")
})
- "render content-length for a strict response
entity".inAssertAllStagesStopped(new HeadRequestSetup {
+ // RFC 9110 section 9.3.2: the server MUST NOT send content in a
response to a HEAD request. The headers are
+ // still rendered as they would be for a GET, so that the peer learns
the size of the resource.
+ "not send the response entity as DATA
frames".inAssertAllStagesStopped(new HeadRequestSetup {
sendHeadRequest()
user.emitResponse(TheStreamId,
HttpResponse(entity =
HttpEntity(ContentTypes.`application/octet-stream`, ByteString("abcde"))))
- val pairs = network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId, endStream = false).toMap
+ val pairs = network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId, endStream = true).toMap
pairs should contain("content-length" -> "5")
pairs should contain("content-type" -> "application/octet-stream")
+ network.expectNoBytes(100.millis)
})
+ "not send DATA frames for a streamed response entity and cancel
it".inAssertAllStagesStopped(
+ new HeadRequestSetup {
+ sendHeadRequest()
+ val entityDataOut = TestPublisher.probe[ByteString]()
+ user.emitResponse(TheStreamId,
+ HttpResponse(entity =
HttpEntity(ContentTypes.`application/octet-stream`,
+ Source.fromPublisher(entityDataOut))))
+
+ network.expectDecodedResponseHEADERSPairs(streamId = TheStreamId,
endStream = true)
+ entityDataOut.expectCancellation()
+ network.expectNoBytes(100.millis)
+ })
+
// the HTTP/1.1 equivalent is "to a HEAD request setting a custom
Content-Type and Content-Length
// (default response entity)" in ResponseRendererSpec: it lets a handler
answer a HEAD request with the
// size of the hypothetical GET response without producing the bytes
@@ -430,49 +446,71 @@ class Http2ServerSpec extends
Http2SpecWithMaterializer("""
user.emitResponse(TheStreamId,
HttpResponse(entity =
HttpEntity.Default(ContentTypes.`application/octet-stream`, 100, Source.empty)))
- network.expectDecodedResponseHEADERSPairs(streamId = TheStreamId,
endStream = false).toMap should contain(
+ network.expectDecodedResponseHEADERSPairs(streamId = TheStreamId,
endStream = true).toMap should contain(
"content-length" -> "100")
+ network.expectNoBytes(100.millis)
})
- // FIXME: RFC 9110 section 9.3.2 says the server MUST NOT send content
in a response to a HEAD request, but
- // the HTTP/2 engine has no notion of the request method on the response
path (ResponseRendering only ever
- // sees the HttpResponse plus its stream id), so the entity is emitted
as DATA frames. HTTP/1.1 strips it in
- // HttpResponseRendererFactory. This test pins the current wire
behaviour so that a fix has to flip it
- // deliberately rather than silently.
- "send the response entity as DATA frames (should not, see RFC 9110
section 9.3.2)".inAssertAllStagesStopped(
+ "translate HEAD to GET when transparent-head-requests is enabled and
still strip the body".inAssertAllStagesStopped(
new HeadRequestSetup {
- sendHeadRequest()
+ override def settings: ServerSettings =
super.settings.withTransparentHeadRequests(true)
+
+ sendHeadRequest().method shouldBe HttpMethods.GET
+
user.emitResponse(TheStreamId,
HttpResponse(entity =
HttpEntity(ContentTypes.`application/octet-stream`, ByteString("abcde"))))
-
- network.expectDecodedResponseHEADERSPairs(streamId = TheStreamId,
endStream = false)
- network.expectDATA(TheStreamId, endStream = true,
ByteString("abcde"))
+ network.expectDecodedResponseHEADERSPairs(streamId = TheStreamId,
endStream = true).toMap should contain(
+ "content-length" -> "5")
+ network.expectNoBytes(100.millis)
})
- // FIXME: `transparent-head-requests` is only applied by
HttpServerBluePrint, so it has no effect over
- // HTTP/2 and the handler always sees a HEAD request. Pinned here so the
divergence from HTTP/1.1 is visible.
- "ignore transparent-head-requests and pass HEAD through to the
handler".inAssertAllStagesStopped(
- new HeadRequestSetup {
- override def settings: ServerSettings =
super.settings.withTransparentHeadRequests(true)
+ "keep HEAD when transparent-head-requests is
disabled".inAssertAllStagesStopped(new HeadRequestSetup {
+ sendHeadRequest().method shouldBe HttpMethods.HEAD
- sendHeadRequest().method shouldBe HttpMethods.HEAD
+ user.emitResponse(TheStreamId, HttpResponse())
+ network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap should contain(":status" -> "200")
+ })
+ }
- user.emitResponse(TheStreamId, HttpResponse())
- network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap should contain(":status" -> "200")
- })
+ // The status based rules are the ones PR #962 introduced for HTTP/1.1 in
HttpMethod.contentLengthAllowed. They
+ // do not depend on the request method, so they are exercised here with a
plain GET.
+ "render content-length according to the response status" should {
+ abstract class GetRequestSetup extends TestSetup with
RequestResponseProbes {
+ val TheStreamId = 1
+ def responsePairs(response: HttpResponse): Map[String, String] = {
+ network.sendRequest(TheStreamId,
+ HttpRequest(HttpMethods.GET, "https://www.example.com/", protocol
= HttpProtocols.`HTTP/2.0`))
+ user.expectRequest()
+ user.emitResponse(TheStreamId, response)
+ network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap
+ }
+ }
- // FIXME: HttpMessageRendering.addContentHeaders renders content-length
straight from the entity and never
- // consults HttpMethod.contentLengthAllowed, so a 304 gets
`content-length: 0` where HTTP/1.1 omits the
- // header entirely (RFC 9110 section 15.4.5: a 304 should carry the
Content-Length a 200 would have had).
- "render content-length 0 for a 304 response (HTTP/1.1 omits
it)".inAssertAllStagesStopped(
- new HeadRequestSetup {
- sendHeadRequest()
- user.emitResponse(TheStreamId, HttpResponse(StatusCodes.NotModified))
+ "render it for a 200 with an empty entity".inAssertAllStagesStopped(new
GetRequestSetup {
+ responsePairs(HttpResponse()) should contain("content-length" -> "0")
+ })
- val pairs = network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap
- pairs should contain(":status" -> "304")
- pairs should contain("content-length" -> "0")
- })
+ "not render it for a 204".inAssertAllStagesStopped(new GetRequestSetup {
+ val pairs = responsePairs(HttpResponse(StatusCodes.NoContent))
+ pairs should contain(":status" -> "204")
+ pairs.keySet should not contain "content-length"
+ })
+
+ // 205 is deliberately not exempt: RFC 9112 section 6.3 only lets 1xx,
204 and 304 be self delimiting, so a 205
+ // has to be framed. This is the case PR #962 fixed for HTTP/1.1 and the
one http4s/http4s#7919 reports.
+ "render it for a 205".inAssertAllStagesStopped(new GetRequestSetup {
+ val pairs = responsePairs(HttpResponse(StatusCodes.ResetContent))
+ pairs should contain(":status" -> "205")
+ pairs should contain("content-length" -> "0")
+ })
+
+ // RFC 9110 section 15.4.5: a 304 is supposed to carry the
content-length a 200 would have had, so rendering a
+ // zero here would be actively misleading.
+ "not render it for a 304".inAssertAllStagesStopped(new GetRequestSetup {
+ val pairs = responsePairs(HttpResponse(StatusCodes.NotModified))
+ pairs should contain(":status" -> "304")
+ pairs.keySet should not contain "content-length"
+ })
}
def requestTests(minCollectStrictEntityBytes: Int) = {
diff --git
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
index 0d4d2f23b..e972a87e1 100644
---
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
+++
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
@@ -132,6 +132,17 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer
with Inside with Insp
// pseudo-header field that appears in a header block after a regular
// header field MUST be treated as malformed...
+ // Neither the CONNECT method of RFC 9113 section 8.5 nor the extended
CONNECT of RFC 8441 is supported: a
+ // CONNECT request omits ":scheme" and ":path", which the parser rejects
as mandatory. Pinned here so that the
+ // CONNECT specific rule in HttpMethods.contentLengthAllowed is
understood to be unreachable over HTTP/2.
+ "not accept a CONNECT request" in {
+ val ex = parseExpectProtocolError(
+ keyValuePairs = Vector(
+ ":method" -> "CONNECT",
+ ":authority" -> "www.example.com:443"))
+ ex.getMessage should ===("Malformed request: Mandatory pseudo-header
':scheme' missing")
+ }
+
"not accept pseudo-header fields after regular headers" in {
val pseudoHeaders = Vector(
":method" -> "GET",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]