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

自定义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事件:

  1. 认证失败时,框架首先生成401 Unauthorized的原始响应,此时会触发第一次ResponseSent;
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 22:23:30