=htp #20052 headerValueByType now works with custom headers
This commit is contained in:
parent
f34ef537f7
commit
d83a323549
10 changed files with 106 additions and 19 deletions
|
|
@ -7,6 +7,7 @@ package docs.http.scaladsl.server.directives
|
|||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.MissingHeaderRejection
|
||||
import akka.http.scaladsl.server.Route
|
||||
import akka.http.scaladsl.server.util.ClassMagnet
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
|
|
|
|||
|
|
@ -294,6 +294,8 @@ Strict-Transport-Security
|
|||
|
||||
__ @github@/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala#L422
|
||||
|
||||
.. _custom-headers-scala:
|
||||
|
||||
Custom Headers
|
||||
--------------
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ the given type is found the request is rejected with a ``MissingHeaderRejection`
|
|||
If the header is expected to be missing in some cases or to customize handling when the header
|
||||
is missing use the :ref:`-optionalHeaderValueByType-` directive instead.
|
||||
|
||||
.. note::
|
||||
Custom headers will only be matched by this directive if they extend ``ModeledCustomHeader``
|
||||
and provide a companion extending ``ModeledCustomHeaderCompanion``, otherwise the routing
|
||||
infrastructure does now know where to search for the needed companion and header name.
|
||||
|
||||
To learn more about defining custom headers, read: :ref:`custom-headers-scala`.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ Optionally extracts the value of the HTTP request header of the given type.
|
|||
The ``optionalHeaderValueByType`` directive is similar to the :ref:`-headerValueByType-` directive but always extracts
|
||||
an ``Option`` value instead of rejecting the request if no matching header could be found.
|
||||
|
||||
.. note::
|
||||
Custom headers will only be matched by this directive if they extend ``ModeledCustomHeader``
|
||||
and provide a companion extending ``ModeledCustomHeaderCompanion``, otherwise the routing
|
||||
infrastructure does now know where to search for the needed companion and header name.
|
||||
|
||||
To learn more about defining custom headers, read: :ref:`custom-headers-scala`.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ abstract class ModeledCustomHeaderCompanion[H <: ModeledCustomHeader[H]] {
|
|||
case _ ⇒ None
|
||||
}
|
||||
|
||||
final implicit val implicitlyLocatableCompanion: ModeledCustomHeaderCompanion[H] = this
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -380,7 +380,6 @@ class HttpExtensionApiSpec extends WordSpec with Matchers with BeforeAndAfterAll
|
|||
|
||||
"create an outgoing connection (with 6 parameters)" in {
|
||||
val (host, port, binding) = runServer()
|
||||
println("host = " + host)
|
||||
val flow = http.outgoingConnection(
|
||||
toHost(host, port),
|
||||
Optional.empty(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package akka.http.scaladsl.server
|
|||
|
||||
import akka.http.scaladsl.model.{ HttpHeader, StatusCodes }
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.directives.HeaderMagnet
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Success, Failure, Try }
|
||||
|
|
@ -13,20 +14,26 @@ import scala.util.{ Success, Failure, Try }
|
|||
object ModeledCustomHeaderSpec {
|
||||
|
||||
//#modeled-api-key-custom-header
|
||||
object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] {
|
||||
def renderInRequests = false
|
||||
def renderInResponses = false
|
||||
override val name = "apiKey"
|
||||
override def parse(value: String) = Try(new ApiTokenHeader(value))
|
||||
}
|
||||
final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] {
|
||||
def renderInRequests = false
|
||||
def renderInResponses = false
|
||||
override val companion = ApiTokenHeader
|
||||
override def value: String = token
|
||||
}
|
||||
object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] {
|
||||
def renderInRequests = false
|
||||
def renderInResponses = false
|
||||
override val name = "apiKey"
|
||||
override def parse(value: String) = Try(new ApiTokenHeader(value))
|
||||
}
|
||||
//#modeled-api-key-custom-header
|
||||
|
||||
final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] {
|
||||
def renderInRequests = false
|
||||
def renderInResponses = false
|
||||
override val companion = DifferentHeader
|
||||
override def value = token
|
||||
}
|
||||
object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] {
|
||||
def renderInRequests = false
|
||||
def renderInResponses = false
|
||||
|
|
@ -35,12 +42,6 @@ object ModeledCustomHeaderSpec {
|
|||
if (value contains " ") Failure(new Exception("Contains illegal whitespace!"))
|
||||
else Success(new DifferentHeader(value))
|
||||
}
|
||||
final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] {
|
||||
def renderInRequests = false
|
||||
def renderInResponses = false
|
||||
override val companion = DifferentHeader
|
||||
override def value = token
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +109,27 @@ class ModeledCustomHeaderSpec extends RoutingSpec {
|
|||
//#matching-in-routes
|
||||
}
|
||||
|
||||
"be able to extract in routing DSL via headerValueByType" in {
|
||||
val routes = headerValueByType[ApiTokenHeader]() { token ⇒
|
||||
val ApiTokenHeader(t) = token
|
||||
complete(s"extracted> $t")
|
||||
}
|
||||
|
||||
Get().withHeaders(RawHeader("apiKey", "TheKey")) ~> routes ~> check {
|
||||
status should ===(StatusCodes.OK)
|
||||
responseAs[String] should ===("extracted> apiKey: TheKey")
|
||||
}
|
||||
|
||||
Get().withHeaders(RawHeader("somethingElse", "TheKey")) ~> routes ~> check {
|
||||
rejection should ===(MissingHeaderRejection("ApiTokenHeader"))
|
||||
}
|
||||
|
||||
Get().withHeaders(ApiTokenHeader("TheKey")) ~> routes ~> check {
|
||||
status should ===(StatusCodes.OK)
|
||||
responseAs[String] should ===("extracted> apiKey: TheKey")
|
||||
}
|
||||
}
|
||||
|
||||
"fail with useful message when unable to parse" in {
|
||||
val ex = intercept[Exception] {
|
||||
DifferentHeader("Hello world") // illegal " "
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ package akka.http.javadsl.server
|
|||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.{ server, Http }
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.http.scaladsl.server.RouteResult
|
||||
import akka.http.impl.server.RouteImplementation
|
||||
import akka.stream.{ ActorMaterializer, Materializer }
|
||||
import akka.stream.scaladsl.{ Keep, Sink }
|
||||
|
|
@ -39,7 +38,7 @@ trait HttpServiceBase {
|
|||
|
||||
import system.dispatcher
|
||||
val r: server.Route = RouteImplementation(route)
|
||||
Http(system).bind(interface, port).toMat(Sink.foreach(_.handleWith(RouteResult.route2HandlerFlow(r))))(Keep.left).run()(materializer).toJava
|
||||
Http(system).bind(interface, port).toMat(Sink.foreach(_.handleWith(akka.http.scaladsl.server.RouteResult.route2HandlerFlow(r))))(Keep.left).run()(materializer).toJava
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import akka.http.javadsl.model.HttpHeader
|
|||
import akka.http.javadsl.server.RequestVal
|
||||
import akka.http.scaladsl.model
|
||||
import akka.http.scaladsl.server.Directive1
|
||||
import akka.http.scaladsl.server.util.ClassMagnet
|
||||
import akka.http.scaladsl.server.directives.HeaderMagnet
|
||||
|
||||
import scala.compat.java8.OptionConverters._
|
||||
import scala.reflect.{ ClassTag, classTag }
|
||||
|
|
@ -31,7 +31,7 @@ object Headers {
|
|||
HeaderImpl[HttpHeader](name, _ ⇒ optionalHeaderInstanceByName(name.toLowerCase()).map(_.asScala), classTag[HttpHeader])
|
||||
|
||||
def byClass[T <: HttpHeader](clazz: Class[T]): Header[T] =
|
||||
HeaderImpl[T](clazz.getSimpleName, ct ⇒ optionalHeaderValueByType(ClassMagnet(ct)), ClassTag(clazz))
|
||||
HeaderImpl[T](clazz.getSimpleName, ct ⇒ optionalHeaderValueByType(HeaderMagnet.fromUnit(())(ct)), ClassTag(clazz))
|
||||
|
||||
private def optionalHeaderInstanceByName(lowercaseName: String): Directive1[Optional[model.HttpHeader]] =
|
||||
extract(_.request.headers.collectFirst {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@
|
|||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.javadsl.model.headers.CustomHeader
|
||||
import akka.http.scaladsl.model.headers.{ModeledCustomHeaderCompanion, ModeledCustomHeader, RawHeader}
|
||||
|
||||
import scala.annotation.implicitNotFound
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.control.NonFatal
|
||||
import akka.http.javadsl.{ model => jm }
|
||||
import akka.http.scaladsl.server.util.ClassMagnet
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
|
|
@ -55,8 +61,11 @@ trait HeaderDirectives {
|
|||
/**
|
||||
* Extracts the first HTTP request header of the given type.
|
||||
* If no header with a matching type is found the request is rejected with a [[akka.http.scaladsl.server.MissingHeaderRejection]].
|
||||
*
|
||||
* Custom headers will only be matched by this directive if they extend [[ModeledCustomHeader]]
|
||||
* and provide a companion extending [[ModeledCustomHeaderCompanion]].
|
||||
*/
|
||||
def headerValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[T] =
|
||||
def headerValueByType[T](magnet: HeaderMagnet[T]): Directive1[T] =
|
||||
headerValuePF(magnet.extractPF) | reject(MissingHeaderRejection(magnet.runtimeClass.getSimpleName))
|
||||
|
||||
//#optional-header
|
||||
|
|
@ -97,8 +106,11 @@ trait HeaderDirectives {
|
|||
|
||||
/**
|
||||
* Extract the header value of the optional HTTP request header with the given type.
|
||||
*
|
||||
* Custom headers will only be matched by this directive if they extend [[ModeledCustomHeader]]
|
||||
* and provide a companion extending [[ModeledCustomHeaderCompanion]].
|
||||
*/
|
||||
def optionalHeaderValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[Option[T]] =
|
||||
def optionalHeaderValueByType[T <: HttpHeader](magnet: HeaderMagnet[T]): Directive1[Option[T]] =
|
||||
optionalHeaderValuePF(magnet.extractPF)
|
||||
|
||||
private def optionalValue(lowerCaseName: String): HttpHeader ⇒ Option[String] = {
|
||||
|
|
@ -108,3 +120,40 @@ trait HeaderDirectives {
|
|||
}
|
||||
|
||||
object HeaderDirectives extends HeaderDirectives
|
||||
|
||||
trait HeaderMagnet[T] {
|
||||
def classTag: ClassTag[T]
|
||||
def runtimeClass: Class[T]
|
||||
|
||||
/**
|
||||
* Returns a partial function that checks if the input value is of runtime type
|
||||
* T and returns the value if it does. Doesn't take erased information into account.
|
||||
*/
|
||||
def extractPF: PartialFunction[HttpHeader, T]
|
||||
}
|
||||
object HeaderMagnet extends LowPriorityHeaderMagnetImplicits {
|
||||
|
||||
/**
|
||||
* If possible we want to apply the special logic for [[ModeledCustomHeader]] to extract custom headers by type,
|
||||
* otherwise the default `fromUnit` is good enough (for headers that the parser emits in the right type already).
|
||||
*/
|
||||
implicit def fromUnitForModeledCustomHeader[T <: ModeledCustomHeader[T], H <: ModeledCustomHeaderCompanion[T]]
|
||||
(u: Unit)(implicit tag: ClassTag[T], companion: ModeledCustomHeaderCompanion[T]): HeaderMagnet[T] =
|
||||
new HeaderMagnet[T] {
|
||||
override def runtimeClass = tag.runtimeClass.asInstanceOf[Class[T]]
|
||||
override def classTag = tag
|
||||
override def extractPF = {
|
||||
case h if h.is(companion.lowercaseName) => companion.apply(h.toString)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait LowPriorityHeaderMagnetImplicits {
|
||||
implicit def fromUnit[T <: HttpHeader](u: Unit)(implicit tag: ClassTag[T]): HeaderMagnet[T] =
|
||||
new HeaderMagnet[T] {
|
||||
val classTag: ClassTag[T] = tag
|
||||
val runtimeClass: Class[T] = tag.runtimeClass.asInstanceOf[Class[T]]
|
||||
val extractPF: PartialFunction[Any, T] = { case x: T ⇒ x }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue