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 09213dd23 test: add HTTP/2 coverage for the HEAD method (#1238)
09213dd23 is described below
commit 09213dd239a8894359a59e04479b7d3fa39afc2a
Author: PJ Fanning <[email protected]>
AuthorDate: Sun Aug 30 11:57:21 2026 +0100
test: add HTTP/2 coverage for the HEAD method (#1238)
Motivation:
The HTTP/2 engine had no test coverage for HEAD requests at all, and no HEAD
handling either: `grep HEAD` over the http2 engine and its tests returns
only
HEADERS frame matches. That left three divergences from HTTP/1.1 invisible.
Modification:
Add a "support HEAD requests" section to Http2ServerSpec covering request
delivery, content-length rendering for strict and known-length entities, the
response body, `transparent-head-requests`, and a 304 response. Three of the
tests pin behaviour that is currently wrong and carry a FIXME naming the
cause
and the relevant RFC, so that a fix has to flip the assertion deliberately
rather than silently:
- the response entity is emitted as DATA frames, although RFC 9110 section
9.3.2 says a server MUST NOT send content in a response to HEAD.
ResponseRendering only ever sees the HttpResponse plus its stream id, so
the
engine cannot know the request was a HEAD.
- `transparent-head-requests` is applied only by HttpServerBluePrint, so it
has
no effect over HTTP/2 and the handler always sees a HEAD request.
- 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.
The content-length tests confirm HTTP/2 already does the right thing for the
object-store use case from #1236.
Result:
HEAD over HTTP/2 is covered, and the three gaps are recorded as assertions
instead of being absent.
Tests:
- sbt "http2-tests / Test / testOnly
org.apache.pekko.http.impl.engine.http2.Http2ServerSpec" - 118 passed, 16
pending
- scalafmt --mode diff-ref=upstream/main - clean
- git diff --check - clean
References:
Refs #1236, Refs #1237
---
.../http/impl/engine/http2/Http2ServerSpec.scala | 83 ++++++++++++++++++++++
1 file changed, 83 insertions(+)
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 1d478726d..d403c0e44 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
@@ -392,6 +392,89 @@ class Http2ServerSpec extends Http2SpecWithMaterializer("""
})
}
+ "support HEAD requests" should {
+ abstract class HeadRequestSetup extends TestSetup with
RequestResponseProbes {
+ val TheStreamId = 1
+ def sendHeadRequest(): HttpRequest = {
+ network.sendRequest(TheStreamId,
+ HttpRequest(HttpMethods.HEAD, "https://www.example.com/", protocol
= HttpProtocols.`HTTP/2.0`))
+ user.expectRequest()
+ }
+ }
+
+ "deliver the request to the handler with an empty
entity".inAssertAllStagesStopped(new HeadRequestSetup {
+ val request = sendHeadRequest()
+ request.method shouldBe HttpMethods.HEAD
+ request.entity shouldBe HttpEntity.Empty
+
+ user.emitResponse(TheStreamId, HttpResponse())
+ network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap should contain(":status" -> "200")
+ })
+
+ "render content-length for a strict response
entity".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
+ pairs should contain("content-length" -> "5")
+ pairs should contain("content-type" -> "application/octet-stream")
+ })
+
+ // 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
+ "render content-length for a response entity with a known length but no
bytes".inAssertAllStagesStopped(
+ new HeadRequestSetup {
+ sendHeadRequest()
+ user.emitResponse(TheStreamId,
+ HttpResponse(entity =
HttpEntity.Default(ContentTypes.`application/octet-stream`, 100, Source.empty)))
+
+ network.expectDecodedResponseHEADERSPairs(streamId = TheStreamId,
endStream = false).toMap should contain(
+ "content-length" -> "100")
+ })
+
+ // 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(
+ new HeadRequestSetup {
+ sendHeadRequest()
+ 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"))
+ })
+
+ // 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)
+
+ sendHeadRequest().method shouldBe HttpMethods.HEAD
+
+ user.emitResponse(TheStreamId, HttpResponse())
+ network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap should contain(":status" -> "200")
+ })
+
+ // 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))
+
+ val pairs = network.expectDecodedResponseHEADERSPairs(streamId =
TheStreamId).toMap
+ pairs should contain(":status" -> "304")
+ pairs should contain("content-length" -> "0")
+ })
+ }
+
def requestTests(minCollectStrictEntityBytes: Int) = {
abstract class RequestEntityTestSetup extends TestSetup with
RequestResponseProbes {
override def settings: ServerSettings =
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]