Quarkus REST流式接口:流处理崩溃时如何返回错误?
在Quarkus中实现流式CSV导出并处理错误状态码
核心问题分析
当流式输出开始后,HTTP响应头(包括200状态码)已经发送给客户端,此时无法再修改状态码。因此处理思路分为两种:
- 确保流处理中抛出的异常被JAX-RS容器捕获,在响应头未发送前返回500;
- 使用Quarkus/Vert.x的低级API,控制响应发送时机,在出错时灵活处理状态码。
方案一:增强StreamingOutput的异常处理
直接在StreamingOutput的写入逻辑中捕获异常,抛出标准的WebApplicationException,让JAX-RS容器自动处理为500状态码。如果响应头还未发送,客户端会收到500;如果已发送部分数据,客户端会收到中断的文件并感知传输错误。
@GET @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("/export/csv") public Response streamCsv() { StreamingOutput streamingOutput = os -> { try { productManager.exportCsv(new RequestContext(1, 1), os); } catch (Exception e) { // 抛出带状态码的WebApplicationException,触发容器返回500 throw new WebApplicationException("CSV导出失败", e, Response.Status.INTERNAL_SERVER_ERROR); } }; return Response.ok(streamingOutput) .header("Content-Disposition", "attachment; filename=product_export.csv") .build(); }
方案二:使用Quarkus ResponseEmitter(推荐)
ResponseEmitter是Quarkus提供的异步流式响应API,能更灵活地控制响应发送时机,在流处理出错时,若响应头未发送则返回500,若已发送则中断连接。
@GET @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("/export/csv") public void streamCsv(@Context ResponseEmitter emitter) { // 先准备响应头,暂不发送 MultivaluedMap<String, Object> headers = Response.ok() .header("Content-Disposition", "attachment; filename=product_export.csv") .build().getHeaders(); emitter.onChunk(() -> { try (OutputStream os = emitter.createOutputStream()) { productManager.exportCsv(new RequestContext(1, 1), os); return false; // 返回false表示流结束 } catch (Exception e) { // 出错时发送错误响应 emitter.error(Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity("导出失败:" + e.getMessage()) .build()); return true; // 返回true表示处理终止 } }); // 发送响应头(此时才正式向客户端发送200状态码) emitter.sendHeaders(headers); }
方案三:直接使用Vert.x RoutingContext
如果需要最底层的控制,可以直接操作Vert.x的RoutingContext,手动管理响应状态和流输出:
@GET @Produces(MediaType.APPLICATION_OCTET_STREAM) @Path("/export/csv") public void streamCsv(@Context RoutingContext ctx) { HttpServerResponse response = ctx.response(); response.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM); response.putHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=product_export.csv"); try { OutputStream os = response.outputStream(); productManager.exportCsv(new RequestContext(1, 1), os); os.flush(); response.end(); } catch (Exception e) { if (!response.headWritten()) { // 响应头未发送,设置500状态码并返回错误信息 response.setStatusCode(500); response.end("CSV导出失败:" + e.getMessage()); } else { // 响应头已发送,只能关闭连接,客户端会感知到传输异常 response.close(); } } }
关键注意事项
- 无论哪种方案,若已经向客户端写入了部分数据,都无法再修改已发送的200状态码,只能中断连接,客户端会收到不完整的文件并感知传输错误;
- 必须确保
productManager.exportCsv方法不会内部吞掉异常,所有错误都要抛出,才能被上层处理; - 所有方案都遵循“不加载全部数据到内存”的要求,仅流式处理小数据块。
内容的提问来源于stack exchange,提问作者funkrusher
相关产品推荐
相关产品推荐

