在 Play 2.6 中,如何编写从 parent 请求转发 headers 的 WS 客户端过滤器?

In Play 2.6, how to write a WS Client filter that forwards headers from a parent request?

如果我有一个名为 HomeController 的控制器接收 GET /foo 和 header X-Foo: Bar 之类的请求,我想创建一个 WS 客户端过滤器,它将读取上下文中的 RequestHeader 并将 header 值复制到传出 WS 请求。

示例控制器:

import play.api.libs.ws.{StandaloneWSRequest, WSClient, WSRequest, WSRequestExecutor, WSRequestFilter}
import play.api.mvc._

import scala.concurrent.ExecutionContext

@Singleton
class HomeController @Inject()(cc: ControllerComponents,
                               myWsClient: MyWSClient)
                              (implicit executionContext: ExecutionContext)
  extends AbstractController(cc) {

  def index = Action.async {
    myWsClient.url("http://www.example.com")
      .get()
      .map(res => Ok(s"${res.status} ${res.statusText}"))(executionContext)
  }
}

引入过滤器的 WSClient 包装器:

@Singleton
class MyWSClient @Inject()(delegate: WSClient, fooBarFilter: FooBarFilter) extends WSClient {
  override def underlying[T]: T = delegate.underlying.asInstanceOf[T]

  override def url(url: String): WSRequest = {
    delegate.url(url)
      .withRequestFilter(fooBarFilter)
  }

  override def close(): Unit = delegate.close()
}

最后是 WS 过滤器本身:

@Singleton
class FooBarFilter extends WSRequestFilter {
  override def apply(executor: WSRequestExecutor): WSRequestExecutor = {
    (request: StandaloneWSRequest) => {
      request.addHttpHeaders(("X-Foo", "<...>")) // INSERT CORRECT VALUE HERE!
      executor.apply(request)
    }
  }
}

最后,期望是请求 GET http://www.example.com 包含 header X-Foo: Bar.

使这更有趣的特殊要求是:

我还没有尝试将它放入实际代码中并测试它是否有效,但这里有一个想法:从 Play 2.1 Http.Context is propagated even across async call. And there is Http.Context._requestHeader 开始看起来像。所以你可以尝试做的是改变 MyWSClientFooBarFilter 像这样:

@Singleton
class MyWSClient @Inject()(delegate: WSClient) extends WSClient {
  override def underlying[T]: T = delegate.underlying.asInstanceOf[T]

  override def url(url: String): WSRequest = {
    val fooHeaderOption = Http.Context.current()._requestHeader().headers.get(FooHeaderFilter.fooHeaderName)
    val baseRequest = delegate.url(url)
    if (fooHeaderOption.isDefined)
      baseRequest.withRequestFilter(new FooHeaderFilter(fooHeaderOption.get))
    else
      baseRequest
  }

  override def close(): Unit = delegate.close()

  class FooHeaderFilter(headerValue: String) extends WSRequestFilter {

    import FooHeaderFilter._

    override def apply(executor: WSRequestExecutor): WSRequestExecutor = {
      (request: StandaloneWSRequest) => {
        request.addHttpHeaders((fooHeaderName, headerValue))
        executor.apply(request)
      }
    }
  }

  object FooHeaderFilter {
    val fooHeaderName = "X-Foo"
  }

}

想法很简单:在创建 WSRequest 时从 Http.Context.current() 中提取 header 并使用 WSRequestFilter

将其附加到请求中

更新:让它在 Scala 中工作 API

正如评论中指出的那样,这种方法在 Scala API 中不起作用,因为 Http.Context 未初始化且未在线程之间传递。要使其工作,需要更高级别的魔法。即你需要:

  1. 简单:过滤器将为 Scala-handled 个请求初始化 Http.Context
  2. 困难:为 Akka's default dispatcher 覆盖 ExecutorServiceConfigurator 以创建将在线程切换之间传递 Http.Context 的自定义 ExecutorService

过滤器很简单:

import play.mvc._
@Singleton
class HttpContextFilter @Inject()(implicit ec: ExecutionContext) extends EssentialFilter {
  override def apply(next: EssentialAction) = EssentialAction { request => {
    Http.Context.current.set(new Http.Context(new Http.RequestImpl(request), null))
    next(request)
  }
  }
}

并将其添加到 application.conf

中的 play.filters.enabled

困难的部分是这样的:

class HttpContextWrapperExecutorService(val delegateEc: ExecutorService) extends AbstractExecutorService {
  override def isTerminated = delegateEc.isTerminated

  override def awaitTermination(timeout: Long, unit: TimeUnit) = delegateEc.awaitTermination(timeout, unit)

  override def shutdownNow() = delegateEc.shutdownNow()

  override def shutdown() = delegateEc.shutdown()

  override def isShutdown = delegateEc.isShutdown

  override def execute(command: Runnable) = {
    val newContext = Http.Context.current.get()
    delegateEc.execute(() => {
      val oldContext = Http.Context.current.get() // might be null!
      Http.Context.current.set(newContext)
      try {
        command.run()
      }
      finally {
        Http.Context.current.set(oldContext)
      }
    })
  }
}


class HttpContextExecutorServiceConfigurator(config: Config, prerequisites: DispatcherPrerequisites) extends ExecutorServiceConfigurator(config, prerequisites) {
  val delegateProvider = new ForkJoinExecutorConfigurator(config.getConfig("fork-join-executor"), prerequisites)

  override def createExecutorServiceFactory(id: String, threadFactory: ThreadFactory): ExecutorServiceFactory = new ExecutorServiceFactory {
    val delegateFactory = delegateProvider.createExecutorServiceFactory(id, threadFactory)

    override def createExecutorService: ExecutorService = new HttpContextWrapperExecutorService(delegateFactory.createExecutorService)
  }
}

并在使用时注册

akka.actor.default-dispatcher.executor = "so.HttpContextExecutorServiceConfigurator"

不要忘记用你的真实包更新“so”。此外,如果您使用更多自定义执行程序或 ExecutionContexts,您还应该修补(包装)它们以在异步调用中传递 Http.Context