Merge pull request #18942 from ktoso/wip-customheaders-impr-ktoso
+htc #18898 modeledCustomHeader to ease matching on headers
This commit is contained in:
commit
fd5a3afc9d
5 changed files with 198 additions and 6 deletions
|
|
@ -216,6 +216,8 @@ header across persistent HTTP connections.
|
||||||
|
|
||||||
.. _RFC 7230: http://tools.ietf.org/html/rfc7230#section-3.3.3
|
.. _RFC 7230: http://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||||
|
|
||||||
|
.. _header-model-scala:
|
||||||
|
|
||||||
Header Model
|
Header Model
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
@ -281,6 +283,38 @@ Connection
|
||||||
|
|
||||||
__ @github@/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala#L422
|
__ @github@/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala#L422
|
||||||
|
|
||||||
|
Custom Headers
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Sometimes you may need to model a custom header type which is not part of HTTP and still be able to use it
|
||||||
|
as convienient as is possible with the built-in types.
|
||||||
|
|
||||||
|
Because of the number of ways one may interact with headers (i.e. try to match a ``CustomHeader`` against a ``RawHeader``
|
||||||
|
or the other way around etc), a helper trait for custom Header types and their companions classes are provided by Akka HTTP.
|
||||||
|
Thanks to extending :class:`ModeledCustomHeader` instead of the plain ``CustomHeader`` such header can be matched
|
||||||
|
|
||||||
|
.. includecode:: ../../../../../akka-http-tests/src/test/scala/akka/http/scaladsl/server/CustomHeaderRoutingSpec.scala
|
||||||
|
:include: modeled-api-key-custom-header
|
||||||
|
|
||||||
|
Which allows the this CustomHeader to be used in the following scenarios:
|
||||||
|
|
||||||
|
.. includecode:: ../../../../../akka-http-tests/src/test/scala/akka/http/scaladsl/server/CustomHeaderRoutingSpec.scala
|
||||||
|
:include: matching-examples
|
||||||
|
|
||||||
|
Including usage within the header directives like in the following :ref:`-headerValuePF-` example:
|
||||||
|
|
||||||
|
.. includecode:: ../../../../../akka-http-tests/src/test/scala/akka/http/scaladsl/server/CustomHeaderRoutingSpec.scala
|
||||||
|
:include: matching-in-routes
|
||||||
|
|
||||||
|
One can also directly extend :class:`CustomHeader` which requires less boilerplate, however that has the downside of
|
||||||
|
matching against :ref:`RawHeader` instances not working out-of-the-box, thus limiting its usefulnes in the routing layer
|
||||||
|
of Akka HTTP. For only rendering such header however it would be enough.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
When defining custom headers, prefer to extend :class:`ModeledCustomHeader` instead of :class:`CustomHeader` directly
|
||||||
|
as it will automatically make your header abide all the expected pattern matching semantics one is accustomed to
|
||||||
|
when using built-in types (such as matching a custom header against a ``RawHeader`` as is often the case in routing
|
||||||
|
layers of Akka HTTP applications).
|
||||||
|
|
||||||
Parsing / Rendering
|
Parsing / Rendering
|
||||||
-------------------
|
-------------------
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ private[http] object HttpHeaderParser {
|
||||||
def prime(parser: HttpHeaderParser): HttpHeaderParser = {
|
def prime(parser: HttpHeaderParser): HttpHeaderParser = {
|
||||||
val valueParsers: Seq[HeaderValueParser] =
|
val valueParsers: Seq[HeaderValueParser] =
|
||||||
HeaderParser.ruleNames.map { name ⇒
|
HeaderParser.ruleNames.map { name ⇒
|
||||||
new ModelledHeaderValueParser(name, parser.settings.maxHeaderValueLength, parser.settings.headerValueCacheLimit(name), parser.settings)
|
new ModeledHeaderValueParser(name, parser.settings.maxHeaderValueLength, parser.settings.headerValueCacheLimit(name), parser.settings)
|
||||||
}(collection.breakOut)
|
}(collection.breakOut)
|
||||||
def insertInGoodOrder(items: Seq[Any])(startIx: Int = 0, endIx: Int = items.size): Unit =
|
def insertInGoodOrder(items: Seq[Any])(startIx: Int = 0, endIx: Int = items.size): Unit =
|
||||||
if (endIx - startIx > 0) {
|
if (endIx - startIx > 0) {
|
||||||
|
|
@ -477,7 +477,7 @@ private[http] object HttpHeaderParser {
|
||||||
def cachingEnabled = maxValueCount > 0
|
def cachingEnabled = maxValueCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private[parsing] class ModelledHeaderValueParser(headerName: String, maxHeaderValueLength: Int, maxValueCount: Int, settings: HeaderParser.Settings)
|
private[parsing] class ModeledHeaderValueParser(headerName: String, maxHeaderValueLength: Int, maxValueCount: Int, settings: HeaderParser.Settings)
|
||||||
extends HeaderValueParser(headerName, maxValueCount) {
|
extends HeaderValueParser(headerName, maxValueCount) {
|
||||||
def apply(hhp: HttpHeaderParser, input: ByteString, valueStart: Int, onIllegalHeader: ErrorInfo ⇒ Unit): (HttpHeader, Int) = {
|
def apply(hhp: HttpHeaderParser, input: ByteString, valueStart: Int, onIllegalHeader: ErrorInfo ⇒ Unit): (HttpHeader, Int) = {
|
||||||
// TODO: optimize by running the header value parser directly on the input ByteString (rather than an extracted String)
|
// TODO: optimize by running the header value parser directly on the input ByteString (rather than an extracted String)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import java.net.InetSocketAddress
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util
|
import java.util
|
||||||
|
|
||||||
|
import akka.event.Logging
|
||||||
|
|
||||||
import scala.reflect.ClassTag
|
import scala.reflect.ClassTag
|
||||||
import scala.util.Try
|
import scala.util.{ Failure, Success, Try }
|
||||||
import scala.annotation.tailrec
|
import scala.annotation.tailrec
|
||||||
import scala.collection.immutable
|
import scala.collection.immutable
|
||||||
|
|
||||||
|
|
@ -48,6 +50,10 @@ sealed trait ModeledHeader extends HttpHeader with Serializable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Superclass for user-defined custom headers defined by implementing `name` and `value`.
|
* Superclass for user-defined custom headers defined by implementing `name` and `value`.
|
||||||
|
*
|
||||||
|
* Prefer to extend [[ModeledCustomHeader]] and [[ModeledCustomHeaderCompanion]] instead if
|
||||||
|
* planning to use the defined header in match clauses (e.g. in the routing layer of Akka HTTP),
|
||||||
|
* as they allow the custom header to be matched from [[RawHeader]] and vice-versa.
|
||||||
*/
|
*/
|
||||||
abstract class CustomHeader extends jm.headers.CustomHeader {
|
abstract class CustomHeader extends jm.headers.CustomHeader {
|
||||||
/** Override to return true if this header shouldn't be rendered */
|
/** Override to return true if this header shouldn't be rendered */
|
||||||
|
|
@ -57,6 +63,43 @@ abstract class CustomHeader extends jm.headers.CustomHeader {
|
||||||
final def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
|
final def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be extended by companion object of a custom header extending [[ModeledCustomHeader]].
|
||||||
|
* Implements necessary apply and unapply methods to make the such defined header feel "native".
|
||||||
|
*/
|
||||||
|
abstract class ModeledCustomHeaderCompanion[H <: ModeledCustomHeader[H]] {
|
||||||
|
def name: String
|
||||||
|
def lowercaseName: String = name.toRootLowerCase
|
||||||
|
|
||||||
|
def parse(value: String): Try[H]
|
||||||
|
|
||||||
|
def apply(value: String): H =
|
||||||
|
parse(value) match {
|
||||||
|
case Success(parsed) ⇒ parsed
|
||||||
|
case Failure(ex) ⇒ throw new IllegalArgumentException(s"Unable to construct custom header by parsing: '$value'", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
def unapply(h: HttpHeader): Option[String] = h match {
|
||||||
|
case _: RawHeader ⇒ if (h.lowercaseName == lowercaseName) Some(h.value) else None
|
||||||
|
case _: CustomHeader ⇒ if (h.lowercaseName == lowercaseName) Some(h.value) else None
|
||||||
|
case _ ⇒ None
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support class for building user-defined custom headers defined by implementing `name` and `value`.
|
||||||
|
* By implementing a [[ModeledCustomHeader]] instead of [[CustomHeader]] directly, all needed unapply
|
||||||
|
* methods are provided for this class, such that it can be pattern matched on from [[RawHeader]] and
|
||||||
|
* the other way around as well.
|
||||||
|
*/
|
||||||
|
abstract class ModeledCustomHeader[H <: ModeledCustomHeader[H]] extends CustomHeader { this: H ⇒
|
||||||
|
def companion: ModeledCustomHeaderCompanion[H]
|
||||||
|
|
||||||
|
final override def name = companion.name
|
||||||
|
final override def lowercaseName: String = name.toRootLowerCase
|
||||||
|
}
|
||||||
|
|
||||||
import akka.http.impl.util.JavaMapping.Implicits._
|
import akka.http.impl.util.JavaMapping.Implicits._
|
||||||
|
|
||||||
// http://tools.ietf.org/html/rfc7230#section-6.1
|
// http://tools.ietf.org/html/rfc7230#section-6.1
|
||||||
|
|
@ -139,6 +182,10 @@ final case class RawHeader(name: String, value: String) extends jm.headers.RawHe
|
||||||
val lowercaseName = name.toRootLowerCase
|
val lowercaseName = name.toRootLowerCase
|
||||||
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
|
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
|
||||||
}
|
}
|
||||||
|
object RawHeader {
|
||||||
|
def unapply[H <: HttpHeader](customHeader: H): Option[(String, String)] =
|
||||||
|
Some(customHeader.name -> customHeader.value)
|
||||||
|
}
|
||||||
|
|
||||||
// http://tools.ietf.org/html/rfc7231#section-5.3.2
|
// http://tools.ietf.org/html/rfc7231#section-5.3.2
|
||||||
object Accept extends ModeledCompanion[Accept] {
|
object Accept extends ModeledCompanion[Accept] {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.http.scaladsl.server
|
||||||
|
|
||||||
|
import akka.http.scaladsl.model.{ HttpHeader, StatusCodes }
|
||||||
|
import akka.http.scaladsl.model.headers._
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import scala.util.{ Success, Failure, Try }
|
||||||
|
|
||||||
|
object ModeledCustomHeaderSpec {
|
||||||
|
|
||||||
|
//#modeled-api-key-custom-header
|
||||||
|
object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] {
|
||||||
|
override val name = "apiKey"
|
||||||
|
override def parse(value: String) = Try(new ApiTokenHeader(value))
|
||||||
|
}
|
||||||
|
final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] {
|
||||||
|
override val companion = ApiTokenHeader
|
||||||
|
override def value: String = token
|
||||||
|
}
|
||||||
|
//#modeled-api-key-custom-header
|
||||||
|
|
||||||
|
object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] {
|
||||||
|
override val name = "different"
|
||||||
|
override def parse(value: String) =
|
||||||
|
if (value contains " ") Failure(new Exception("Contains illegal whitespace!"))
|
||||||
|
else Success(new DifferentHeader(value))
|
||||||
|
}
|
||||||
|
final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] {
|
||||||
|
override val companion = DifferentHeader
|
||||||
|
override def value = token
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModeledCustomHeaderSpec extends RoutingSpec {
|
||||||
|
import ModeledCustomHeaderSpec._
|
||||||
|
|
||||||
|
"CustomHeader" should {
|
||||||
|
|
||||||
|
"be able to be extracted using expected syntax" in {
|
||||||
|
//#matching-examples
|
||||||
|
val ApiTokenHeader(t1) = ApiTokenHeader("token")
|
||||||
|
t1 should ===("token")
|
||||||
|
|
||||||
|
val RawHeader(k2, v2) = ApiTokenHeader("token")
|
||||||
|
k2 should ===("apiKey")
|
||||||
|
v2 should ===("token")
|
||||||
|
|
||||||
|
// will match, header keys are case insensitive
|
||||||
|
val ApiTokenHeader(v3) = RawHeader("APIKEY", "token")
|
||||||
|
v3 should ===("token")
|
||||||
|
|
||||||
|
intercept[MatchError] {
|
||||||
|
// won't match, different header name
|
||||||
|
val ApiTokenHeader(v4) = DifferentHeader("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept[MatchError] {
|
||||||
|
// won't match, different header name
|
||||||
|
val RawHeader("something", v5) = DifferentHeader("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
intercept[MatchError] {
|
||||||
|
// won't match, different header name
|
||||||
|
val ApiTokenHeader(v6) = RawHeader("different", "token")
|
||||||
|
}
|
||||||
|
//#matching-examples
|
||||||
|
}
|
||||||
|
|
||||||
|
"be able to match from RawHeader" in {
|
||||||
|
|
||||||
|
//#matching-in-routes
|
||||||
|
def extractFromCustomHeader = headerValuePF {
|
||||||
|
case t @ ApiTokenHeader(token) ⇒ s"extracted> $t"
|
||||||
|
case raw: RawHeader ⇒ s"raw> $raw"
|
||||||
|
}
|
||||||
|
|
||||||
|
val routes = extractFromCustomHeader { s ⇒
|
||||||
|
complete(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
Get().withHeaders(RawHeader("apiKey", "TheKey")) ~> routes ~> check {
|
||||||
|
status should ===(StatusCodes.OK)
|
||||||
|
responseAs[String] should ===("extracted> apiKey: TheKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
Get().withHeaders(RawHeader("somethingElse", "TheKey")) ~> routes ~> check {
|
||||||
|
status should ===(StatusCodes.OK)
|
||||||
|
responseAs[String] should ===("raw> somethingElse: TheKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
Get().withHeaders(ApiTokenHeader("TheKey")) ~> routes ~> check {
|
||||||
|
status should ===(StatusCodes.OK)
|
||||||
|
responseAs[String] should ===("extracted> apiKey: TheKey")
|
||||||
|
}
|
||||||
|
//#matching-in-routes
|
||||||
|
}
|
||||||
|
|
||||||
|
"fail with useful message when unable to parse" in {
|
||||||
|
val ex = intercept[Exception] {
|
||||||
|
DifferentHeader("Hello world") // illegal " "
|
||||||
|
}
|
||||||
|
|
||||||
|
ex.getMessage should ===("Unable to construct custom header by parsing: 'Hello world'")
|
||||||
|
ex.getCause.getMessage should include("whitespace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -92,9 +92,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
||||||
|filecontent
|
|filecontent
|
||||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")),
|
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")),
|
||||||
Multipart.General.BodyPart.Strict(
|
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "filecontent"), List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||||
HttpEntity(`application/octet-stream`, "filecontent"),
|
|
||||||
List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
|
||||||
}
|
}
|
||||||
"illegal headers" in (
|
"illegal headers" in (
|
||||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue