petitviolet blog

    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