blog.petitviolet.net

Akka-HTTP - カスタムヘッダーの取り扱い方

2016-05-19

QiitaScalaAkkaAkka-HTTP

標準で用意されていない独自のヘッダーを定義して使いたいケース。 大雑把に、文字列として扱う場合と型で扱う場合に分ける

文字列で扱う

headerValueByNameで良い。 引数にSymbolStringを渡せば、それにマッチするヘッダーの値が取り出せる。

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

型で扱う

headerValueByPFheaderValueByTypeのどちらかを使えば出来る。 以下の様なリクエスト/レスポンスになるように実装する。

$ curl localhost:8080/header/original -H 'My-Header: yes'
original => My-Header: token(yes)

まず、カスタムヘッダーとなる型を用意する。 ModeledCustomHeaderModeledCustomHeaderCompanionを実装する。

// 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 のOriginalHeaderClassMagnet[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