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 9bbb67cb8 give ParsingErrorHandler access to the request that was
rejected (#1245) (#1246)
9bbb67cb8 is described below
commit 9bbb67cb8a8b9d395308b9c54b060c8da4e15624
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Sep 11 09:57:16 2026 +0100
give ParsingErrorHandler access to the request that was rejected (#1245)
(#1246)
`ParsingErrorHandler` could not see the request it was rejecting: none of
its four arguments describe the method or the request target, and
`ParserOutput.MessageStartError` discarded both at the point where the
parser gave up.
Add `IllegalRequestContext` and a five-argument `handle` overload that
receives it. The overload defaults to the existing four-argument method,
so existing handlers, `DefaultParsingErrorHandler` included, keep working
unchanged.
The parser populates the context at the failure site: its `method`,
`uri` and `uriBytes` fields are reused across a keep-alive connection, so
they are cleared for every message to stop a rejection from reporting the
previous request's values.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../illegal-request-context.excludes | 25 ++++++
.../apache/pekko/http/ParsingErrorHandler.scala | 98 +++++++++++++++++++++-
.../client/OutgoingConnectionBlueprint.scala | 2 +-
.../impl/engine/parsing/HttpMessageParser.scala | 26 +++++-
.../impl/engine/parsing/HttpRequestParser.scala | 17 ++++
.../http/impl/engine/parsing/ParserOutput.scala | 6 +-
.../impl/engine/server/HttpServerBluePrint.scala | 18 ++--
.../impl/engine/ws/WebSocketClientBlueprint.scala | 2 +-
.../impl/engine/parsing/RequestParserSpec.scala | 49 ++++++++++-
.../impl/engine/parsing/ResponseParserSpec.scala | 2 +-
.../http/impl/engine/server/HttpServerSpec.scala | 69 ++++++++++++++-
11 files changed, 295 insertions(+), 19 deletions(-)
diff --git
a/http-core/src/main/mima-filters/2.0.x.backwards.excludes/illegal-request-context.excludes
b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/illegal-request-context.excludes
new file mode 100644
index 000000000..a7049a8bf
--- /dev/null
+++
b/http-core/src/main/mima-filters/2.0.x.backwards.excludes/illegal-request-context.excludes
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# internal API: MessageStartError carries what is known about the request that
was rejected
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.ParserOutput#MessageStartError.copy")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.ParserOutput#MessageStartError.this")
+ProblemFilters.exclude[MissingTypesProblem]("org.apache.pekko.http.impl.engine.parsing.ParserOutput$MessageStartError$")
+ProblemFilters.exclude[DirectMissingMethodProblem]("org.apache.pekko.http.impl.engine.parsing.ParserOutput#MessageStartError.apply")
+ProblemFilters.exclude[IncompatibleSignatureProblem]("org.apache.pekko.http.impl.engine.parsing.ParserOutput#MessageStartError.unapply")
+
+# internal API: replaced by an instance level completion handling that can
report the same context
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/ParsingErrorHandler.scala
b/http-core/src/main/scala/org/apache/pekko/http/ParsingErrorHandler.scala
index 6bd426cc0..472519d29 100644
--- a/http-core/src/main/scala/org/apache/pekko/http/ParsingErrorHandler.scala
+++ b/http-core/src/main/scala/org/apache/pekko/http/ParsingErrorHandler.scala
@@ -13,19 +13,115 @@
package org.apache.pekko.http
+import java.util.Optional
+
+import scala.jdk.OptionConverters._
+
import org.apache.pekko
import pekko.event.LoggingAdapter
import pekko.http.javadsl.{ model => jm }
-import pekko.http.scaladsl.model.{ ErrorInfo, HttpResponse, StatusCode }
+import pekko.http.scaladsl.model.{ ErrorInfo, HttpMethod, HttpProtocol,
HttpResponse, StatusCode }
import pekko.http.scaladsl.settings.ServerSettings
+/**
+ * What is known about a request that failed to parse, at the point where
parsing gave up.
+ *
+ * Every field is optional because a request can be rejected before that part
of it has been read:
+ * a request with an unsupported method fails before the request target is
seen, and one with an
+ * unparsable request target fails before the protocol is seen.
+ *
+ * Note that `rawRequestTarget` is unvalidated, attacker-controlled input, by
definition malformed
+ * whenever the rejection was caused by the request target itself. Anything
that logs or echoes it
+ * has to escape it.
+ *
+ * @since 2.0.0
+ */
+final class IllegalRequestContext private[http] (
+ val method: Option[HttpMethod],
+ val rawRequestTarget: Option[String],
+ val protocol: Option[HttpProtocol]) {
+
+ /**
+ * Java API
+ *
+ * @since 2.0.0
+ */
+ def getMethod: Optional[jm.HttpMethod] = method.map(m => m:
jm.HttpMethod).toJava
+
+ /**
+ * Java API
+ *
+ * @since 2.0.0
+ */
+ def getRawRequestTarget: Optional[String] = rawRequestTarget.toJava
+
+ /**
+ * Java API
+ *
+ * @since 2.0.0
+ */
+ def getProtocol: Optional[jm.HttpProtocol] = protocol.map(p => p:
jm.HttpProtocol).toJava
+
+ override def toString: String =
+ s"IllegalRequestContext(${method.map(_.value).getOrElse("-")}," +
+
s"${rawRequestTarget.getOrElse("-")},${protocol.map(_.value).getOrElse("-")})"
+}
+
+object IllegalRequestContext {
+
+ /**
+ * A context that knows nothing about the request, used when no information
could be recovered.
+ *
+ * @since 2.0.0
+ */
+ val empty: IllegalRequestContext = new IllegalRequestContext(None, None,
None)
+
+ private[http] def apply(
+ method: Option[HttpMethod],
+ rawRequestTarget: Option[String],
+ protocol: Option[HttpProtocol]): IllegalRequestContext =
+ if (method.isEmpty && rawRequestTarget.isEmpty && protocol.isEmpty) empty
+ else new IllegalRequestContext(method, rawRequestTarget, protocol)
+}
+
+/**
+ * Produces the response to a request that failed to parse. Selected by the
+ * `pekko.http.server.parsing-error-handler` setting.
+ *
+ * This is also the earliest public symbol that observes a rejected request,
so it is what
+ * observability tooling attaches to: the OpenTelemetry Java agent instruments
`handle` to emit a
+ * span for a request that never reaches the route handler
+ * (open-telemetry/opentelemetry-java-instrumentation#5139).
+ */
abstract class ParsingErrorHandler {
def handle(status: StatusCode, error: ErrorInfo, log: LoggingAdapter,
settings: ServerSettings): jm.HttpResponse
+
+ /**
+ * Called by the server for a request that failed to parse, with what is
known about that request.
+ *
+ * The default implementation ignores `context` and delegates to the
four-argument `handle`, so
+ * existing handlers keep working unchanged; override this method instead to
make use of the
+ * context. The parameter is still worth passing for a handler that does not
read it, because the
+ * arguments of this method are visible to anything instrumenting it: the
OpenTelemetry Java agent
+ * has to name the span it emits for a rejected request `HTTP` and report
neither
+ * `http.request.method` nor `url.path`, since the four-argument signature
describes only the
+ * failure and never the request that caused it. See
+ * [[https://github.com/apache/pekko-http/issues/1245]].
+ *
+ * Note that `DefaultParsingErrorHandler` deliberately keeps implementing
the four-argument method
+ * rather than this one, so that advice matching that signature keeps firing.
+ *
+ * @since 2.0.0
+ */
+ def handle(status: StatusCode, error: ErrorInfo, log: LoggingAdapter,
settings: ServerSettings,
+ context: IllegalRequestContext): jm.HttpResponse =
+ handle(status, error, log, settings)
}
object DefaultParsingErrorHandler extends ParsingErrorHandler {
import pekko.http.impl.engine.parsing.logParsingError
+ // implements the four-argument method on purpose, see the scaladoc of the
five-argument one
override def handle(
status: StatusCode, info: ErrorInfo, log: LoggingAdapter, settings:
ServerSettings): HttpResponse = {
logParsingError(
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/client/OutgoingConnectionBlueprint.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/client/OutgoingConnectionBlueprint.scala
index 6672d7446..b0839294d 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/client/OutgoingConnectionBlueprint.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/client/OutgoingConnectionBlueprint.scala
@@ -201,7 +201,7 @@ private[http] object OutgoingConnectionBlueprint {
push(httpResponseOut, new HttpResponse(statusCode, headers,
attributes, entity, protocol))
completeOnMessageEnd = closeRequested
- case MessageStartError(_, info) =>
+ case MessageStartError(_, info, _) =>
throw IllegalResponseException(info)
case other =>
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpMessageParser.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpMessageParser.scala
index ac468de2b..fa9961286 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpMessageParser.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpMessageParser.scala
@@ -22,6 +22,7 @@ import org.parboiled2.CharUtils
import org.apache.pekko
import pekko.annotation.InternalApi
+import pekko.http.IllegalRequestContext
import pekko.http.impl.model.parser.CharacterClasses
import pekko.http.impl.util.HttpConstants._
import pekko.http.scaladsl.model.{ ParsingException => _, _ }
@@ -62,6 +63,25 @@ private[http] trait HttpMessageParser[Output >:
MessageOutput <: ParserOutput] {
/** invoked if the specified protocol is unknown */
protected def onBadProtocol(input: ByteString): Nothing
+
+ /**
+ * What is known about the message that is currently being parsed, at the
point where parsing failed.
+ * Only the request parser has anything to report here.
+ */
+ protected def illegalRequestContext: IllegalRequestContext =
IllegalRequestContext.empty
+
+ /** The protocol of the message that is currently being parsed */
+ protected final def currentProtocol: HttpProtocol = protocol
+
+ /**
+ * Completion handling for a message start that was truncated by the
connection closing, reporting
+ * what the parser had already read of that message.
+ */
+ protected final val completionIsMessageStartError: CompletionHandling =
+ () =>
+ Some(MessageStartError(StatusCodes.BadRequest, ErrorInfo("Illegal HTTP
message start"),
+ illegalRequestContext))
+
protected def parseMessage(input: ByteString, offset: Int):
HttpMessageParser.StateResult
protected def parseEntity(headers: List[HttpHeader], protocol: HttpProtocol,
input: ByteString, bodyStart: Int,
clh: Option[`Content-Length`], cth: Option[`Content-Type`], isChunked:
Boolean,
@@ -122,7 +142,7 @@ private[http] trait HttpMessageParser[Output >:
MessageOutput <: ParserOutput] {
}
protected final def startNewMessage(input: ByteString, offset: Int):
StateResult = {
- if (offset < input.length)
setCompletionHandling(CompletionIsMessageStartError)
+ if (offset < input.length)
setCompletionHandling(completionIsMessageStartError)
try parseMessage(input, offset)
catch { case NotEnoughDataException => continue(input,
offset)(startNewMessage) }
}
@@ -382,7 +402,7 @@ private[http] trait HttpMessageParser[Output >:
MessageOutput <: ParserOutput] {
protected final def failMessageStart(status: StatusCode, summary: String,
detail: String = ""): StateResult =
failMessageStart(status, ErrorInfo(summary, detail))
protected final def failMessageStart(status: StatusCode, info: ErrorInfo):
StateResult = {
- emit(MessageStartError(status, info))
+ emit(MessageStartError(status, info, illegalRequestContext))
setCompletionHandling(CompletionOk)
terminate()
}
@@ -452,8 +472,6 @@ private[http] object HttpMessageParser {
type CompletionHandling = () => Option[ErrorOutput]
val CompletionOk: CompletionHandling = () => None
- val CompletionIsMessageStartError: CompletionHandling =
- () => Some(ParserOutput.MessageStartError(StatusCodes.BadRequest,
ErrorInfo("Illegal HTTP message start")))
val CompletionIsEntityStreamError: CompletionHandling =
() =>
Some(ParserOutput.EntityStreamError(ErrorInfo(
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpRequestParser.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpRequestParser.scala
index a826738cd..e43fe3954 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpRequestParser.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/HttpRequestParser.scala
@@ -14,12 +14,14 @@
package org.apache.pekko.http.impl.engine.parsing
import java.lang.{ StringBuilder => JStringBuilder }
+import java.nio.charset.StandardCharsets
import javax.net.ssl.SSLSession
import scala.annotation.{ switch, tailrec }
import org.apache.pekko
import pekko.annotation.InternalApi
+import pekko.http.IllegalRequestContext
import pekko.http.impl.engine.server.HttpAttributes
import pekko.http.impl.util.ByteStringParserInput
import pekko.http.impl.util.HttpConstants._
@@ -69,6 +71,8 @@ private[http] final class HttpRequestParser(
private var method: HttpMethod = null
private var uri: Uri = null
private var uriBytes: ByteString = null
+ // whether `protocol` of the underlying parser belongs to the message
currently being parsed
+ private var protocolParsed: Boolean = false
override def onPush(): Unit =
handleParserOutput(parseSessionBytes(grab(in)))
override def onPull(): Unit = handleParserOutput(doPull())
@@ -89,9 +93,16 @@ private[http] final class HttpRequestParser(
override def parseMessage(input: ByteString, offset: Int): StateResult =
if (offset < input.length) {
+ // the fields below are reused for every message on a connection,
forget what the previous
+ // one left behind so that a failure cannot report values belonging
to another request
+ method = null
+ uri = null
+ uriBytes = null
+ protocolParsed = false
var cursor = parseMethod(input, offset)
cursor = parseRequestTarget(input, cursor)
cursor = parseProtocol(input, cursor)
+ protocolParsed = true
if (byteAt(input, cursor) == CR_BYTE && byteAt(input, cursor + 1) ==
LF_BYTE)
parseHeaderLines(input, cursor + 2)
else if (byteAt(input, cursor) == LF_BYTE)
@@ -252,6 +263,12 @@ private[http] final class HttpRequestParser(
}
} else failMessageStart("Request is missing required `Host` header")
+ override protected def illegalRequestContext: IllegalRequestContext =
+ IllegalRequestContext(
+ Option(method),
+ Option.unless(uriBytes eq
null)(uriBytes.decodeString(StandardCharsets.US_ASCII)),
+ Option.when(protocolParsed)(currentProtocol))
+
private def remoteAddressStr: String =
inheritedAttributes.get[HttpAttributes.RemoteAddress].map(_.address)
match {
case Some(addr) => s" from ${addr.getHostString}:${addr.getPort}"
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParserOutput.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParserOutput.scala
index 6582407de..60ceeb6d3 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParserOutput.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/parsing/ParserOutput.scala
@@ -16,6 +16,7 @@ package org.apache.pekko.http.impl.engine.parsing
import org.apache.pekko
import pekko.NotUsed
import pekko.annotation.InternalApi
+import pekko.http.IllegalRequestContext
import pekko.http.impl.util.StreamUtils
import pekko.http.scaladsl.model._
import pekko.stream.scaladsl.Source
@@ -62,7 +63,10 @@ private[http] object ParserOutput {
final case class EntityChunk(chunk: HttpEntity.ChunkStreamPart) extends
MessageOutput
- final case class MessageStartError(status: StatusCode, info: ErrorInfo)
extends MessageStart with ErrorOutput
+ final case class MessageStartError(
+ status: StatusCode,
+ info: ErrorInfo,
+ context: IllegalRequestContext = IllegalRequestContext.empty) extends
MessageStart with ErrorOutput
final case class EntityStreamError(info: ErrorInfo) extends ErrorOutput
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/server/HttpServerBluePrint.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/server/HttpServerBluePrint.scala
index 4c20f5bd1..9a8c48795 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/server/HttpServerBluePrint.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/server/HttpServerBluePrint.scala
@@ -26,7 +26,7 @@ import pekko.stream._
import pekko.stream.TLSProtocol._
import pekko.stream.scaladsl._
import pekko.stream.stage._
-import pekko.http.ParsingErrorHandler
+import pekko.http.{ IllegalRequestContext, ParsingErrorHandler }
import pekko.http.scaladsl.settings.ServerSettings
import pekko.http.impl.engine.parsing.ParserOutput._
import pekko.http.impl.engine.parsing._
@@ -268,14 +268,15 @@ private[http] object HttpServerBluePrint {
def establishAbsoluteUri(requestOutput: RequestOutput): RequestOutput =
requestOutput match {
case connect: RequestStart if connect.method == HttpMethods.CONNECT =>
MessageStartError(StatusCodes.BadRequest,
- ErrorInfo(s"CONNECT requests are not supported", s"Rejecting CONNECT
request to '${connect.uri}'"))
+ ErrorInfo(s"CONNECT requests are not supported", s"Rejecting CONNECT
request to '${connect.uri}'"),
+ contextOf(connect))
case start: RequestStart =>
try {
val effectiveUri = HttpRequest.effectiveUri(start.uri,
start.headers, isSecureConnection, defaultHostHeader)
start.copy(uri = effectiveUri)
} catch {
case e: IllegalUriException =>
- MessageStartError(StatusCodes.BadRequest, e.info)
+ MessageStartError(StatusCodes.BadRequest, e.info, contextOf(start))
}
case x => x
}
@@ -283,6 +284,9 @@ private[http] object HttpServerBluePrint {
Flow[SessionBytes].via(rootParser).map(establishAbsoluteUri)
}
+ private def contextOf(start: RequestStart): IllegalRequestContext =
+ IllegalRequestContext(Some(start.method), Some(start.uri.toString),
Some(start.protocol))
+
def rendering(settings: ServerSettings, log: LoggingAdapter,
dateHeaderRendering: DateHeaderRendering)
: Flow[ResponseRenderingContext, ResponseRenderingOutput, NotUsed] = {
import settings._
@@ -482,7 +486,8 @@ private[http] object HttpServerBluePrint {
case MessageEnd =>
messageEndPending = false
push(requestPrepOut, MessageEnd)
- case MessageStartError(status, info)
=> finishWithIllegalRequestError(status, info)
+ case MessageStartError(status, info, context) =>
+ finishWithIllegalRequestError(status, info, context)
case x: EntityStreamError if messageEndPending &&
openRequests.isEmpty =>
// client terminated the connection after receiving an early
response to 100-continue
completeStage()
@@ -587,8 +592,9 @@ private[http] object HttpServerBluePrint {
}
})
- def finishWithIllegalRequestError(status: StatusCode, info:
ErrorInfo): Unit = {
- val errorResponse =
JavaMapping.toScala(parsingErrorHandler.handle(status, info, log, settings))
+ def finishWithIllegalRequestError(status: StatusCode, info: ErrorInfo,
+ context: IllegalRequestContext = IllegalRequestContext.empty):
Unit = {
+ val errorResponse =
JavaMapping.toScala(parsingErrorHandler.handle(status, info, log, settings,
context))
emitErrorResponse(errorResponse)
}
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocketClientBlueprint.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocketClientBlueprint.scala
index 34e34b349..34b4e97d6 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocketClientBlueprint.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/ws/WebSocketClientBlueprint.scala
@@ -139,7 +139,7 @@ private[http] object WebSocketClientBlueprint {
result.success(InvalidUpgradeResponse(response,
s"WebSocket server at $uri returned $problem"))
failStage(new IllegalArgumentException(s"WebSocket upgrade
did not finish because of '$problem'"))
}
- case MessageStartError(statusCode, errorInfo) =>
+ case MessageStartError(statusCode, errorInfo, _) =>
throw new IllegalStateException(s"Message failed with status
code $statusCode; Error info: $errorInfo")
case other =>
throw new IllegalStateException(s"unexpected element of type
${other.getClass}")
diff --git
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
index af81c84c0..9e85e2342 100644
---
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
+++
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/RequestParserSpec.scala
@@ -26,6 +26,7 @@ import pekko.stream.TLSProtocol._
import org.scalatest.matchers.Matcher
import org.scalatest.BeforeAndAfterAll
import pekko.http.scaladsl.settings.{ ParserSettings, WebSocketSettings }
+import pekko.http.IllegalRequestContext
import pekko.http.impl.engine.parsing.ParserOutput._
import pekko.http.impl.settings.WebSocketSettingsImpl
import pekko.http.impl.util._
@@ -684,7 +685,7 @@ abstract class RequestParserSpec(mode: String, newLine:
String) extends AnyFreeS
val result = multiParse(newParser)(Seq("GET /\u0000HTTP/1.1
HTTP/1.1\r\n"))
result.length shouldEqual 1
result.head match {
- case Left(MessageStartError(BadRequest, info)) =>
+ case Left(MessageStartError(BadRequest, info, _)) =>
info.summary should startWith("Illegal request-target")
case other => fail(s"Expected BadRequest MessageStartError but got
$other")
}
@@ -862,6 +863,31 @@ abstract class RequestParserSpec(mode: String, newLine:
String) extends AnyFreeS
|""" should parseToError(BadRequest, ErrorInfo("A chunked request
must not contain a Content-Length header"))
}
}
+
+ "report what it knows about a rejected request" - {
+ "the method and the raw request target when the request target is
illegal" in new Test {
+ illegalRequestContextOf("GET /%% HTTP/1.1\r\n") should be(
+ (Some(GET), Some("/%%"), None))
+ }
+
+ "the method, the raw request target and the protocol when the request
line parsed" in new Test {
+ illegalRequestContextOf("GET /abc HTTP/1.1\r\n\r\n") should be(
+ (Some(GET), Some("/abc"), Some(`HTTP/1.1`)))
+ }
+
+ "nothing when the request failed before the method was known" in new
Test {
+ illegalRequestContextOf("BLAH /abc HTTP/1.1\r\n") should be((None,
None, None))
+ }
+
+ "nothing from the previous request on the same connection" in new Test {
+ // the parser reuses its fields for every message, a rejection must
not report the previous request
+ multiParse(newParser)(Seq("GET /previous HTTP/1.1\r\nHost: x\r\n\r\n",
"BLAH /abc HTTP/1.1\r\n")) match {
+ case Seq(Right(_), Left(MessageStartError(_, _, context))) =>
+ (context.method, context.rawRequestTarget, context.protocol)
should be((None, None, None))
+ case other => fail(s"Expected a request followed by a
MessageStartError but got $other")
+ }
+ }
+ }
}
override def afterAll() = TestKit.shutdownActorSystem(system)
@@ -881,6 +907,14 @@ abstract class RequestParserSpec(mode: String, newLine:
String) extends AnyFreeS
override def toString = req.toString
}
+ /** The context of the single `MessageStartError` that parsing `input` is
expected to produce */
+ def illegalRequestContextOf(input: String): (Option[HttpMethod],
Option[String], Option[HttpProtocol]) =
+ multiParse(newParser)(Seq(input)) match {
+ case Seq(Left(MessageStartError(_, _, context))) =>
+ (context.method, context.rawRequestTarget, context.protocol)
+ case other => fail(s"Expected a single MessageStartError but got
$other")
+ }
+
def strictEqualify[T](x: Either[T, HttpRequest]): Either[T,
StrictEqualHttpRequest] =
x.map(new StrictEqualHttpRequest(_))
@@ -908,7 +942,16 @@ abstract class RequestParserSpec(mode: String, newLine:
String) extends AnyFreeS
parser: HttpRequestParser,
expected: Either[RequestOutput, HttpRequest]*): Matcher[Seq[String]] =
equal(expected.map(strictEqualify))
- .matcher[Seq[Either[RequestOutput,
StrictEqualHttpRequest]]].compose(multiParse(parser))
+ .matcher[Seq[Either[RequestOutput, StrictEqualHttpRequest]]]
+ // the illegal request context is asserted separately, it is not part
of what these expectations describe
+ .compose(multiParse(parser)(_).map(withoutIllegalRequestContext))
+
+ def withoutIllegalRequestContext(
+ output: Either[RequestOutput, StrictEqualHttpRequest]):
Either[RequestOutput, StrictEqualHttpRequest] =
+ output match {
+ case Left(error: MessageStartError) => Left(error.copy(context =
IllegalRequestContext.empty))
+ case other => other
+ }
def multiParse(parser: HttpRequestParser)(input: Seq[String]):
Seq[Either[RequestOutput, StrictEqualHttpRequest]] =
Source(input.toList)
@@ -920,7 +963,7 @@ abstract class RequestParserSpec(mode: String, newLine:
String) extends AnyFreeS
case (Seq(RequestStart(method, uri, protocol, attrs, headers,
createEntity, _, close)), entityParts) =>
closeAfterResponseCompletion :+= close
Right(HttpRequest(method, uri, headers, createEntity(entityParts),
protocol))
- case (Seq(x @ (MessageStartError(_, _) | EntityStreamError(_))),
rest) =>
+ case (Seq(x @ (MessageStartError(_, _, _) | EntityStreamError(_))),
rest) =>
rest.runWith(Sink.cancelled)
Left(x)
}
diff --git
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
index cf0ef5acb..46cbbd507 100644
---
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
+++
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/parsing/ResponseParserSpec.scala
@@ -446,7 +446,7 @@ abstract class ResponseParserSpec(mode: String, newLine:
String) extends PekkoSp
case (Seq(ResponseStart(statusCode, protocol, attributes, headers,
createEntity, close)), entityParts) =>
closeAfterResponseCompletion :+= close
Right(new HttpResponse(statusCode, headers, attributes,
createEntity(entityParts), protocol))
- case (Seq(x @ (MessageStartError(_, _) | EntityStreamError(_))),
tail) =>
+ case (Seq(x @ (MessageStartError(_, _, _) | EntityStreamError(_))),
tail) =>
tail.runWith(Sink.ignore)
Left(x)
}.concatSubstreams
diff --git
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/server/HttpServerSpec.scala
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/server/HttpServerSpec.scala
index 6d73f2967..e92cde84c 100644
---
a/http-core/src/test/scala/org/apache/pekko/http/impl/engine/server/HttpServerSpec.scala
+++
b/http-core/src/test/scala/org/apache/pekko/http/impl/engine/server/HttpServerSpec.scala
@@ -16,7 +16,7 @@ package org.apache.pekko.http.impl.engine.server
import org.apache.pekko
import pekko.actor.ActorSystem
import pekko.event.LoggingAdapter
-import pekko.http.ParsingErrorHandler
+import pekko.http.{ IllegalRequestContext, ParsingErrorHandler }
import pekko.http.impl.engine.ws.ByteStringSinkProbe
import pekko.http.impl.util._
import pekko.http.scaladsl.Http.ServerLayer
@@ -48,6 +48,20 @@ object TestParsingErrorHandler extends ParsingErrorHandler {
HttpResponse(StatusCodes.ImATeapot, entity = HttpEntity("Tea hea"))
}
+/** Renders what the handler was told about the request that was rejected */
+object ContextReportingParsingErrorHandler extends ParsingErrorHandler {
+ override def handle(
+ status: StatusCode, error: ErrorInfo, log: LoggingAdapter, settings:
ServerSettings): HttpResponse =
+ HttpResponse(status, entity = HttpEntity("no context"))
+
+ override def handle(status: StatusCode, error: ErrorInfo, log:
LoggingAdapter, settings: ServerSettings,
+ context: IllegalRequestContext): HttpResponse =
+ HttpResponse(status,
+ entity = HttpEntity(
+ s"${context.method.map(_.value).getOrElse("-")}
${context.rawRequestTarget.getOrElse("-")} " +
+ s"${context.protocol.map(_.value).getOrElse("-")}"))
+}
+
class HttpServerSpec extends PekkoSpec(
"""pekko.loggers =
["org.apache.pekko.http.impl.util.SilenceAllTestEventListener"]
pekko.loglevel = DEBUG
@@ -1615,6 +1629,59 @@ class HttpServerSpec extends PekkoSpec(
netIn.sendComplete()
netOut.expectComplete()
})
+
+ "pass the rejected request to the parsing error handler" in
assertAllStagesStopped(new TestSetup {
+ override def settings: ServerSettings =
+ super.settings.withParsingErrorHandler(
+
"org.apache.pekko.http.impl.engine.server.ContextReportingParsingErrorHandler$")
+
+ send("""GET /%% HTTP/1.1
+ |Host: www.example.com
+ |
+ |""")
+
+ requests.request(1)
+
+ expectResponseWithWipedDate(
+ """|HTTP/1.1 400 Bad Request
+ |Server: pekko-http/test
+ |Date: XXXX
+ |Connection: close
+ |Content-Type: text/plain; charset=UTF-8
+ |Content-Length: 9
+ |
+ |GET /%% -""")
+
+ netIn.sendComplete()
+ netOut.expectComplete()
+ })
+
+ "pass the rejected request to the parsing error handler when the Host
header does not match" in
+ assertAllStagesStopped(new TestSetup {
+ override def settings: ServerSettings =
+ super.settings.withParsingErrorHandler(
+
"org.apache.pekko.http.impl.engine.server.ContextReportingParsingErrorHandler$")
+
+ send("""GET http://www.example.com/unparsable HTTP/1.1
+ |Host: www.example.net
+ |
+ |""")
+
+ requests.request(1)
+
+ expectResponseWithWipedDate(
+ """|HTTP/1.1 400 Bad Request
+ |Server: pekko-http/test
+ |Date: XXXX
+ |Connection: close
+ |Content-Type: text/plain; charset=UTF-8
+ |Content-Length: 46
+ |
+ |GET http://www.example.com/unparsable HTTP/1.1""")
+
+ netIn.sendComplete()
+ netOut.expectComplete()
+ })
}
class TestSetup(maxContentLength: Int = -1) extends HttpServerTestSetupBase {
implicit def system: ActorSystem = spec.system
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]