自定义Unauthorized状态页触发on(ResponseSent)两次的问题
问题描述
我有一个监听ResponseSent事件的自定义插件,代码如下:
on(ResponseSent) { it.application.log.info("RESPONSE SENT!!!!") }
同时配置了Status Pages插件处理未授权请求:
status(HttpStatusCode.Unauthorized) { call, status -> call.respondText(text = "UNAUTHORIZED!!!", status = status) }
当发起未授权请求时,"RESPONSE SENT!!!!"日志会被打印两次。复现该问题的单元测试如下(移除Status Pages插件后测试通过):
@Test fun responseSentShouldOnlyBeCalledOnceWithStatusPagePluginInstalled() = testApplication { //given var numTimesResponseSentCalled = 0 val ResponseSentPlugin = createApplicationPlugin(name = "ResponseSentPlugin") { on(ResponseSent) { numTimesResponseSentCalled++ } } application { install(StatusPages) { status(HttpStatusCode.Unauthorized) { call, status -> call.respondText(text = "UNAUTHORIZED!", status = status) } } install(ResponseSentPlugin) install(Authentication) { bearer { authenticate { null } } } install(Routing) { authenticate { post("/foo") { call.respondText("bar") } } } } //when client.post("/foo") //then assertEquals(1, numTimesResponseSentCalled) }
原因分析
这种情况的核心原因是Status Pages插件的拦截逻辑会触发两次ResponseSent事件:
- 认证失败时,框架首先生成
401 Unauthorized的原始响应,此时会触发第一次ResponseSent; - Status Pages插件拦截了这个原始响应,取消了它的发送,转而生成并发送自定义的"UNAUTHORIZED!!!"响应,这会触发第二次
ResponseSent。
解决方案
以下是几种可行的解决办法:
方法1:用请求属性标记Status Pages处理过的响应
在Status Pages的处理逻辑中给当前请求添加一个标记,然后在自定义插件的ResponseSent监听中跳过已标记的响应:
首先修改Status Pages配置:
// 定义全局属性键 val STATUS_PAGES_HANDLED = AttributeKey<Boolean>("STATUS_PAGES_HANDLED") status(HttpStatusCode.Unauthorized) { call, status -> call.attributes.put(STATUS_PAGES_HANDLED, true) call.respondText(text = "UNAUTHORIZED!!!", status = status) }
然后修改自定义插件的事件监听:
on(ResponseSent) { call -> val isHandledByStatusPages = call.attributes.getOrNull(STATUS_PAGES_HANDLED) ?: false if (!isHandledByStatusPages) { call.application.log.info("RESPONSE SENT!!!!") } }
方法2:替换事件为ResponseBodyReady
如果业务逻辑允许,可以改用ResponseBodyReady事件替代ResponseSent。这个事件只会在最终要发送的响应准备完毕时触发一次,不会被Status Pages的拦截逻辑重复触发:
on(ResponseBodyReady) { call -> call.application.log.info("RESPONSE SENT!!!!") }
方法3:调整插件安装顺序(可选)
确保自定义插件在Status Pages之后安装,这样插件的事件监听会优先捕获到最终的自定义响应。不过这种方式可靠性不如前两种,建议优先使用方法1或2。
修改后的单元测试使用方法1或2后,numTimesResponseSentCalled会等于1,测试通过。
内容的提问来源于stack exchange,提问作者aalonzo
相关产品推荐
相关产品推荐

