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

RestEasyReactive捕获AuthenticationFailedException时响应已写入问题排查

Quarkus Reactive Resteasy中AuthenticationExceptionMapper触发响应已写入错误的问题

问题背景

搭建了一个简单的Quarkus Reactive Resteasy服务器,核心端点代码如下:

@Path("/")
@ApplicationScoped
public class Hello {

    @GET
    @Path("hello")
    @RolesAllowed({"roleYouNeed"})
    public Uni<Response> hello(){
        return Uni.createFrom().item(Response.ok("hello").build());
    }
}

为统一错误响应格式,创建了多个ExceptionMapper,其中处理未提供JWT令牌场景的UnauthorizedExceptionMapper运行正常:

@Provider//register as JAXRS provider
@Priority(1)//override the build-int mapper(which has 5001 prio)
@Slf4j
public class UnauthorizedExceptionMapper implements ExceptionMapper<UnauthorizedException> {
    @Override
    public Response toResponse(UnauthorizedException exception) {
        log.warn("There was an incoming request that did not have the correct Authorization");
        return Response.status(Response.Status.UNAUTHORIZED)
                .entity(new ErrorResponseObject().addErrorsItem(new ErrorResponseObjectErrorsInner()
                        .code(String.valueOf(Response.Status.UNAUTHORIZED.getStatusCode()))
                        .detail("Unauthorized")))
                .build();
    }

}

但当用户发送无效JWT令牌时,会触发JWTParseException进而抛出AuthenticationFailedException,为此创建了对应的AuthenticationExceptionMapper:

@Provider
@Priority(1)
@Slf4j
public class AuthenticationExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {
    @Override
    public Response toResponse(AuthenticationFailedException exception) {
        log.warn("There was an incoming request that did not have the correct Authorization");
        return Response.status(Response.Status.UNAUTHORIZED)
                .entity(new ErrorResponseObject().addErrorsItem(new ErrorResponseObjectErrorsInner()
                        .code(String.valueOf(Response.Status.UNAUTHORIZED.getStatusCode()))
                        .detail("Unauthorized")))
                .build();
    }
}

该Mapper确实捕获了异常并输出日志:

[com.ene.odp.exc.AuthenticationExceptionMapper] (vert.x-eventloop-thread-0) There was an incoming request that did not have the correct Authorization 

但同时出现响应已写入的错误:

2022-07-25 16:22:03,812 ERROR [org.jbo.res.rea.com.cor.AbstractResteasyReactiveContext] (vert.x-eventloop-thread-1) Request failed: java.io.IOException: java.io.IOException: Failed to write
        at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveOutputStream.close(ResteasyReactiveOutputStream.java:272)
        at io.quarkus.resteasy.reactive.jackson.runtime.serialisers.BasicServerJacksonMessageBodyWriter.writeResponse(BasicServerJacksonMessageBodyWriter.java:44)
        at org.jboss.resteasy.reactive.server.core.ServerSerialisers.invokeWriter(ServerSerialisers.java:207)
        at org.jboss.resteasy.reactive.server.core.serialization.DynamicEntityWriter.write(DynamicEntityWriter.java:104)
        at org.jboss.resteasy.reactive.server.handlers.ResponseWriterHandler.handle(ResponseWriterHandler.java:31)
        at org.jboss.resteasy.reactive.server.handlers.ResponseWriterHandler.handle(ResponseWriterHandler.java:15)
        at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:141)
        at org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext$1$1.handle(VertxResteasyReactiveRequestContext.java:73)
        at org.jboss.resteasy.reactive.server.vertx.VertxResteasyReactiveRequestContext$1$1.handle(VertxResteasyReactiveRequestContext.java:70)
        at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:100)
        at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:63)
        at io.vertx.core.impl.EventLoopContext.lambda$runOnContext$0(EventLoopContext.java:38)
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:503)
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.io.IOException: Failed to write
        at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveOutputStream.write(ResteasyReactiveOutputStream.java:109)
        at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveOutputStream.writeBlocking(ResteasyReactiveOutputStream.java:224)
        at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveOutputStream.close(ResteasyReactiveOutputStream.java:270)
        ... 18 more
Caused by: java.lang.IllegalStateException: Response has already been written
        at io.vertx.core.http.impl.Http1xServerResponse.checkValid(Http1xServerResponse.java:675)
        at io.vertx.core.http.impl.Http1xServerResponse.writeQueueFull(Http1xServerResponse.java:274)
        at io.quarkus.vertx.http.runtime.filters.AbstractResponseWrapper.writeQueueFull(AbstractResponseWrapper.java:486)
        at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveOutputStream.awaitWriteable(ResteasyReactiveOutputStream.java:116)
        at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveOutputStream.write(ResteasyReactiveOutputStream.java:82)

疑问:为何捕获该异常时响应已被返回?如何确保仅返回AuthenticationExceptionMapper中定义的响应?


问题原因

Quarkus的SmallRye JWT扩展在处理无效JWT令牌时,会先于自定义ExceptionMapper尝试写入默认的401响应到输出流。当自定义Mapper捕获到AuthenticationFailedException并尝试写入自定义响应时,输出流已经被占用,从而触发"Response has already been written"错误。

解决方案

方案1:用Quarkus配置直接自定义JWT错误响应

无需编写ExceptionMapper,直接在application.properties中配置JWT相关错误响应:

# 设置错误状态码
quarkus.smallrye-jwt.auth-mechanism-error-code=401
# 设置错误提示信息
quarkus.smallrye-jwt.auth-mechanism-error-message=Invalid or expired JWT token
# 若需要自定义响应体格式,可结合Quarkus错误处理扩展指定默认响应实体
quarkus.resteasy-reactive.error-handling.default-entity=ErrorResponseObject

方案2:调整Mapper优先级并清除已有响应

如果必须使用自定义ExceptionMapper,可将优先级设为比内部处理逻辑更高(比如@Priority(0)),同时在方法中清除已有响应状态:

@Provider
@Priority(0)
@Slf4j
public class AuthenticationExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {
    @Context
    private HttpServletRequest request;

    @Override
    public Response toResponse(AuthenticationFailedException exception) {
        log.warn("Invalid JWT token received");
        // 清除已写入的响应状态
        request.setAttribute("javax.servlet.error.status_code", null);
        return Response.status(Response.Status.UNAUTHORIZED)
                .entity(new ErrorResponseObject().addErrorsItem(new ErrorResponseObjectErrorsInner()
                        .code("401")
                        .detail("Invalid or expired JWT token")))
                .build();
    }
}

方案3:实现ReactiveExceptionMapper适配响应式模型

由于使用的是Reactive Resteasy,推荐实现ReactiveExceptionMapper而非传统ExceptionMapper,它更适配响应式模型,能避免响应重复写入问题:

@Provider
@Priority(1)
@Slf4j
public class AuthenticationExceptionMapper implements ReactiveExceptionMapper<AuthenticationFailedException> {
    @Override
    public Uni<Response> toResponse(AuthenticationFailedException exception) {
        log.warn("Invalid JWT token received");
        return Uni.createFrom().item(Response.status(Response.Status.UNAUTHORIZED)
                .entity(new ErrorResponseObject().addErrorsItem(new ErrorResponseObjectErrorsInner()
                        .code("401")
                        .detail("Invalid or expired JWT token")))
                .build());
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 22:57:20