Akka-HTTP - カスタムヘッダーの取り扱い方
2016-05-19
QiitaScalaAkkaAkka-HTTP標準で用意されていない独自のヘッダーを定義して使いたいケース。 大雑把に、文字列として扱う場合と型で扱う場合に分ける
文字列で扱う
headerValueByNameで良い。
引数にSymbolかStringを渡せば、それにマッチするヘッダーの値が取り出せる。
val route =
  path("header" / "ping") {
    get {
      headerValueByName('Message) { msg: String =>
        complete(s"pong: $msg")
      }
    }
  }
リクエスト/レスポンスはこのようになる。 非常に簡単で良い。
$ curl 'http://localhost:8080/header/ping' -H 'Message: hello'
pong: hello
型で扱う
headerValueByPFとheaderValueByTypeのどちらかを使えば出来る。
以下の様なリクエスト/レスポンスになるように実装する。
$ curl localhost:8080/header/original -H 'My-Header: yes'
original => My-Header: token(yes)
まず、カスタムヘッダーとなる型を用意する。
ModeledCustomHeaderとModeledCustomHeaderCompanionを実装する。
// class for custom header
class OriginalHeader(token: String) extends ModeledCustomHeader[OriginalHeader] {
  override def companion: ModeledCustomHeaderCompanion[OriginalHeader] = OriginalHeader
  override def value(): String = s"token($token)"
}
// companion object
object OriginalHeader extends ModeledCustomHeaderCompanion[OriginalHeader] {
  override def name: String = "My-Header" // class名と違う
  override def parse(value: String): Try[OriginalHeader] = Try(new OriginalHeader(value))
}
クラス名はOriginalHeaderだが、リクエストのヘッダーとしてはMy-Headerで受けることとする。
headerValueByPF を使う
PartialFunction[HttpHeader, T]を定義してやればよい。
val originalHeaderExtract = headerValuePF {
  case h @ OriginalHeader(token) => h
}
val route =
  path("header" / "original") {
    originalHeaderExtract { originalHeader =>
      get {
        complete(s"original => $originalHeader")
      }
    }
  }
headerValueByType を使う
User-Agentとかと同じようにやっても何故か上手くいかない。
headerValueByType[`OriginalHeader`]() { originalHeader => ??? }
そのため、headerValueByTypeの引数に与えるためのClassMagnet[OriginalHeader]を自前で用意する必要がある。
実装例としては以下で、 ClassMagnet#applyとはextractPFの実装を変えている。
もちろん、object のOriginalHeaderでClassMagnet[OriginalHeader]を mix-in しても良い。
val originalHeaderMagnet = new ClassMagnet[OriginalHeader] {
  override def classTag: ClassTag[OriginalHeader] = implicitly[ClassTag[OriginalHeader]]
  override def runtimeClass: Class[OriginalHeader] = classOf[OriginalHeader]
  override def extractPF: PartialFunction[Any, OriginalHeader] = {
    case h: CustomHeader if h.name == OriginalHeader.name => OriginalHeader(h.value())
    case h: RawHeader if h.name == OriginalHeader.name => OriginalHeader(h.value)
  }
}
このClassMagnet[OriginalHeader]をheaderValueByTypeの引数に明示的に与えてやれば良い
val route =
  path("original") {
    headerValueByType[OriginalHeader](originalHeaderMagnet) { originalHeader =>
      get {
        complete(s"original => $originalHeader")
      }
    }
  }
注意点
headerValueByType[T]を使う場合、Tは別のクラスの内部クラスにしてはならない。
Class#getSimpleNameでコケるという意味不明な状況に陥る。
以下に StackTrace を一部抜粋。
java.lang.InternalError: Malformed class name
    at java.lang.Class.getSimpleName(Class.java:1322)
    at akka.http.scaladsl.server.directives.HeaderDirectives$class.headerValueByType(HeaderDirectives.scala:60)
    at akka.http.scaladsl.server.Directives$.headerValueByType(Directives.scala:34)
    at net.petitviolet.application.service.RoutingSampleService$Inner$$anonfun$25$$anonfun$apply$39$$anonfun$apply$40.apply(RoutingSampleService.scala:257
)
    at net.petitviolet.application.service.RoutingSampleService$Inner$$anonfun$25$$anonfun$apply$39$$anonfun$apply$40.apply(RoutingSampleService.scala:257
)
    at akka.http.scaladsl.server.Directive$$anonfun$addByNameNullaryApply$1$$anonfun$apply$13.apply(Directive.scala:136)
    at akka.http.scaladsl.server.Directive$$anonfun$addByNameNullaryApply$1$$anonfun$apply$13.apply(Directive.scala:136)
    at akka.http.scaladsl.server.directives.BasicDirectives$$anonfun$mapRouteResult$1$$anonfun$apply$3.apply(BasicDirectives.scala:32)
    ...
一応防ぐ手段もあって、クラス名をOriginalHeaderではなく$OriginalHeaderにすればInternalErrorは起こらなくなる。
型で縛るカスタムヘッダーまとめ
headerValueByType使う場合でもPartialFunction実装していて、かつheaderValueByTypeの中でheaderValueByPF 使っているので、headerValueByTypeを使う意味があんまり見えないため、headerValueByPFで良さそうかなと。
from: https://qiita.com/petitviolet/items/c5657d21485d6170cda3