Skip to content

Commit 0d94099

Browse files
He-PinCopilot
andcommitted
fix: align HTTP/2 malformed-header handling
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3e64c21 commit 0d94099

4 files changed

Lines changed: 39 additions & 28 deletions

File tree

http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestErrorFlow.scala

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,25 @@ private[http2] final class RequestErrorFlow
4949
BidiShape(responseIn, responseOut, requestIn, requestOut)
5050

5151
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) {
52-
setHandlers(requestIn, requestOut, new InHandler with OutHandler {
53-
override def onPush(): Unit = {
54-
grab(requestIn) match {
55-
case RequestParsing.OkRequest(request) => push(requestOut, request)
56-
case notOk: RequestParsing.BadRequest =>
57-
emit(responseOut,
58-
HttpResponse(StatusCodes.BadRequest, entity = notOk.info.summary).addAttribute(Http2.streamId, notOk.streamId))
59-
pull(requestIn)
52+
setHandlers(requestIn, requestOut,
53+
new InHandler with OutHandler {
54+
override def onPush(): Unit = {
55+
grab(requestIn) match {
56+
case RequestParsing.OkRequest(request) => push(requestOut, request)
57+
case notOk: RequestParsing.BadRequest =>
58+
emit(responseOut,
59+
HttpResponse(StatusCodes.BadRequest, entity = notOk.info.summary).addAttribute(Http2.streamId,
60+
notOk.streamId))
61+
pull(requestIn)
62+
}
6063
}
61-
}
6264

63-
override def onPull(): Unit = pull(requestIn)
64-
})
65-
setHandlers(responseIn, responseOut, new InHandler with OutHandler {
66-
override def onPush(): Unit = push(responseOut, grab(responseIn))
67-
override def onPull(): Unit = if (!hasBeenPulled(responseIn)) pull(responseIn)
68-
})
65+
override def onPull(): Unit = pull(requestIn)
66+
})
67+
setHandlers(responseIn, responseOut,
68+
new InHandler with OutHandler {
69+
override def onPush(): Unit = push(responseOut, grab(responseIn))
70+
override def onPull(): Unit = if (!hasBeenPulled(responseIn)) pull(responseIn)
71+
})
6972
}
7073
}

http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/RequestParsing.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ private[http2] object RequestParsing {
158158
parseError("HTTP message must not contain more than one content-type header", "content-type")
159159

160160
case ":status" =>
161-
parseError("Pseudo-header ':status' is for responses only; it cannot appear in a request", ":status")
161+
protocolError("Pseudo-header ':status' is for responses only; it cannot appear in a request")
162162

163163
case "content-length" =>
164164
if (contentLength == -1) {
@@ -218,18 +218,18 @@ private[http2] object RequestParsing {
218218
private[http2] def checkUniquePseudoHeader(name: String, value: AnyRef): Unit =
219219
if (value ne null) protocolError(s"Pseudo-header '$name' must not occur more than once")
220220
private[http2] def checkNoRegularHeadersBeforePseudoHeader(name: String, seenRegularHeader: Boolean): Unit =
221-
if (seenRegularHeader) parseError(s"Pseudo-header field '$name' must not appear after a regular header", name)
221+
if (seenRegularHeader) protocolError(s"Pseudo-header field '$name' must not appear after a regular header")
222222
private[http2] def validateHeader(httpHeader: HttpHeader) = httpHeader.lowercaseName match {
223223
case "connection" =>
224224
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
225-
parseError("Header 'Connection' must not be used with HTTP/2", "Connection")
225+
protocolError("Header 'Connection' must not be used with HTTP/2")
226226
case "transfer-encoding" =>
227227
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
228-
parseError("Header 'Transfer-Encoding' must not be used with HTTP/2", "Transfer-encoding")
228+
protocolError("Header 'Transfer-Encoding' must not be used with HTTP/2")
229229
case "te" =>
230230
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
231231
if (httpHeader.value.compareToIgnoreCase("trailers") != 0)
232-
parseError(s"Header 'TE' must not contain value other than 'trailers', value was '${httpHeader.value}", "TE")
232+
protocolError(s"Header 'TE' must not contain value other than 'trailers', value was '${httpHeader.value}")
233233
case _ => // ok
234234
}
235235

http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/Http2HeaderParsing.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ package org.apache.pekko.http.impl.engine.http2.hpack
1616
import org.apache.pekko
1717
import pekko.annotation.InternalApi
1818
import pekko.http.impl.engine.http2.RequestParsing
19-
import pekko.http.impl.engine.http2.RequestParsing.parseError
19+
import pekko.http.impl.engine.http2.RequestParsing.{ parseError, protocolError }
2020
import pekko.http.scaladsl.model
2121
import pekko.http.scaladsl.model.HttpHeader.ParsingResult
2222
import model.{ HttpHeader, HttpMethod, HttpMethods, IllegalUriException, ParsingException, StatusCode, Uri }
@@ -41,7 +41,9 @@ private[pekko] object Http2HeaderParsing {
4141
}
4242
object PathAndQuery extends HeaderParser[(Uri.Path, Option[String])](":path") {
4343
override def parse(name: String, value: String, parserSettings: ParserSettings): (Uri.Path, Option[String]) =
44-
try {
44+
if (value.isEmpty) {
45+
protocolError("Pseudo-header ':path' must not be empty")
46+
} else try {
4547
Uri.parseHttp2PathPseudoHeader(value, mode = parserSettings.uriParsingMode)
4648
} catch {
4749
case IllegalUriException(info) => throw new ParsingException(info)

http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,14 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp
118118
// appear in requests.
119119

120120
"not accept response pseudo-header fields in a request" in {
121-
val info = parseExpectError(
121+
val ex = parseExpectProtocolError(
122122
keyValuePairs = Vector(
123123
":scheme" -> "https",
124124
":method" -> "GET",
125125
":path" -> "/",
126126
":status" -> "200"
127127
))
128-
info.summary should
128+
ex.getMessage should
129129
===("Malformed request: Pseudo-header ':status' is for responses only; it cannot appear in a request")
130130
}
131131

@@ -144,7 +144,9 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp
144144
// Insert the Foo header so it occurs before at least one pseudo-header
145145
val (before, after) = pseudoHeaders.splitAt(insertPoint)
146146
val modified = before ++ Vector("Foo" -> "bar") ++ after
147-
parseExpectError(modified)
147+
val ex = parseExpectProtocolError(modified)
148+
ex.getMessage should
149+
===(s"Malformed request: Pseudo-header field '${after.head._1}' must not appear after a regular header")
148150
}
149151
}
150152

@@ -155,26 +157,29 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp
155157

156158
"not accept connection-specific headers" in {
157159
// Add Connection header to indicate that Foo is a connection-specific header
158-
parseExpectError(Vector(
160+
val ex = parseExpectProtocolError(Vector(
159161
":method" -> "GET",
160162
":scheme" -> "https",
161163
":path" -> "/",
162164
"Connection" -> "foo",
163165
"Foo" -> "bar"
164166
))
167+
ex.getMessage should ===("Malformed request: Header 'Connection' must not be used with HTTP/2")
165168
}
166169

167170
"not accept TE with other values than 'trailers'" in {
168171

169172
// The only exception to this is the TE header field, which MAY be
170173
// present in an HTTP/2 request; when it is, it MUST NOT contain any
171174
// value other than "trailers".
172-
parseExpectError(Vector(
175+
val ex = parseExpectProtocolError(Vector(
173176
":method" -> "GET",
174177
":scheme" -> "https",
175178
":path" -> "/",
176179
"TE" -> "chunked"
177180
))
181+
ex.getMessage should
182+
===("Malformed request: Header 'TE' must not contain value other than 'trailers', value was 'chunked")
178183

179184
}
180185

@@ -467,12 +472,13 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer with Inside with Insp
467472
"reject empty ':path' pseudo-headers for http and https" in {
468473
val schemes = Seq("http", "https")
469474
forAll(schemes) { (scheme: String) =>
470-
parseExpectError(
475+
val ex = parseExpectProtocolError(
471476
keyValuePairs = Vector(
472477
":method" -> "POST",
473478
":scheme" -> scheme,
474479
":path" -> ""
475480
))
481+
ex.getMessage should ===("Malformed request: Pseudo-header ':path' must not be empty")
476482
}
477483
}
478484

0 commit comments

Comments
 (0)