You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Play应用代理服务器错误处理测试与逻辑实现问询

解决方案:Play代理服务器故障转移到备用服务器

我来帮你搞定这个Play代理服务器的故障转移问题,同时解决测试环境无法捕获错误的难题!

一、实现自定义错误处理器完成故障转移

首先我们需要扩展Play的默认错误处理器,重写onServerError方法,在服务器A发生错误时自动请求服务器B,并根据B的响应返回结果。这里用Scala代码示例:

import play.api.http.DefaultHttpErrorHandler
import play.api.mvc._
import play.api._
import scala.concurrent._
import play.api.libs.ws._
import scala.concurrent.duration._

class CustomErrorHandler(
  env: Environment,
  config: Configuration,
  sourceMapper: OptionalSourceMapper,
  router: Provider[Router],
  wsClient: WSClient
) extends DefaultHttpErrorHandler(env, config, sourceMapper, router) {

  // 从配置文件读取服务器B的地址,避免硬编码
  private val backendServerUrl = config.get[String]("servers.backend")

  override def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = {
    // 构建转发到服务器B的请求,携带原请求的方法、头信息
    val backendRequest = wsClient.url(s"$backendServerUrl${request.uri}")
      .withHttpHeaders(request.headers.toSimpleMap.toSeq: _*)
      .withRequestTimeout(5.seconds) // 设置超时,避免请求卡住

    // 根据原请求方法执行对应请求
    val responseFuture = request.method match {
      case "GET" => backendRequest.get()
      case "POST" => request.body.asBytes.map(bytes => backendRequest.post(bytes)).getOrElse(backendRequest.post(""))
      case "PUT" => request.body.asBytes.map(bytes => backendRequest.put(bytes)).getOrElse(backendRequest.put(""))
      case other => backendRequest.execute(other)
    }

    responseFuture.map { response =>
      // 原样返回服务器B的响应
      Result(
        response.status,
        response.headers.map { case (k, vs) => (k, vs.toSeq) },
        response.bodyAsSource
      )
    }.recover {
      // 如果服务器B也请求失败,返回Internal Server Error
      case _ => InternalServerError("Service Unavailable")
    }
  }
}

然后在application.conf里注册这个错误处理器:

play.http.errorHandler = "com.yourpackage.CustomErrorHandler"
servers.backend = "http://server-b:8080" # 替换成你的服务器B地址

二、解决测试环境无法捕获错误的问题

Play的测试环境默认会使用专门的错误处理器,不会触发我们自定义的逻辑,所以需要手动配置启用自定义错误处理器:

1. 配置测试环境的错误处理器

在conf/application.test.conf中添加:

play.http.errorHandler = "com.yourpackage.CustomErrorHandler"

2. 编写测试用例验证逻辑

用MockWS模拟服务器B的响应,确保故障转移逻辑正常工作:

import org.scalatestplus.play._
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.test._
import play.api.libs.ws._
import scala.concurrent.Future

class CustomErrorHandlerSpec extends PlaySpec with OneAppPerTest {

  // 自定义MockWS客户端,模拟服务器B的不同响应
  private def mockBackendWS(successResponse: Boolean): WSClient = new WSClient {
    override def url(url: String): WSRequest = new WSRequest {
      override def execute(method: String): Future[WSResponse] = {
        if (successResponse) {
          Future.successful(WSResponse(200, Map.empty, "Response from Server B", ""))
        } else {
          Future.failed(new Exception("Backend server down"))
        }
      }
      // 实现其他WSRequest的必要方法,这里简化处理
      override def get(): Future[WSResponse] = execute("GET")
      override def post(body: String): Future[WSResponse] = execute("POST")
      override def put(body: String): Future[WSResponse] = execute("PUT")
      // 省略其他方法的默认实现...
    }
    override def close(): Unit = ()
  }

  override def fakeApplication(): Application = {
    new GuiceApplicationBuilder()
      .configure("play.http.errorHandler" -> "com.yourpackage.CustomErrorHandler")
      .configure("servers.backend" -> "http://mock-server-b")
      .build()
  }

  "CustomErrorHandler" should {
    "forward request to Server B when Server A throws error" in {
      val app = new GuiceApplicationBuilder()
        .overrides(bind[WSClient].toInstance(mockBackendWS(successResponse = true)))
        .configure("play.http.errorHandler" -> "com.yourpackage.CustomErrorHandler")
        .build()

      running(app) {
        // 模拟一个会抛出服务器错误的请求
        val errorRequest = FakeRequest(GET, "/error-endpoint").withHandler {
          throw new RuntimeException("Server A error")
        }
        val result = route(app, errorRequest).get
        status(result) mustBe OK
        contentAsString(result) mustBe "Response from Server B"
      }
    }

    "return 500 when both Server A and Server B fail" in {
      val app = new GuiceApplicationBuilder()
        .overrides(bind[WSClient].toInstance(mockBackendWS(successResponse = false)))
        .configure("play.http.errorHandler" -> "com.yourpackage.CustomErrorHandler")
        .build()

      running(app) {
        val errorRequest = FakeRequest(GET, "/error-endpoint").withHandler {
          throw new RuntimeException("Server A error")
        }
        val result = route(app, errorRequest).get
        status(result) mustBe INTERNAL_SERVER_ERROR
      }
    }
  }
}

三、额外注意点

  • 请求body转发:如果你的代理需要处理POST/PUT等带body的请求,要确保原请求的body被正确转发给服务器B,代码里已经做了基本处理,你可以根据实际需求调整。
  • 超时与重试:可以给WS请求添加重试逻辑,避免因网络波动导致不必要的500。
  • 日志记录:建议在错误处理器中添加日志,记录服务器A的错误信息和转发到B的请求情况,方便排查问题。

内容的提问来源于stack exchange,提问作者mrdoubtful

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:02:02