SpringBoot OpenApi接口抛异常仍下载空Zip文件问题排查
问题
使用SpringBoot 2.6.13编写了通过消息ID下载多个文件为Zip压缩包的REST接口,控制器代码如下:
@Operation(summary = "Download all attach in a zip by message id.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Attach downloaded.", content = { @Content(schema = @Schema(type = "string", format = "binary")) }), @ApiResponse(responseCode = "404", description = "No attach found.", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Error.class)) }), @ApiResponse(responseCode = "500", description = "Internal Server Error.", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = Error.class)) }) }) @GetMapping("/getMessageAttachs/{id}") public void getAttachmentsByMessage(@PathVariable("id") Integer id, HttpServletResponse response) { response.setContentType("application/zip"); String headerKey = "Content-Disposition"; String headerValue = "attachment; filename=files.zip"; response.setHeader(headerKey, headerValue); threadAttachmentsService.retrieveMessageFiles(response, id); }
服务类retrieveMessageFiles(实际代码中方法名为zipFiles)的实现逻辑如下:
public void zipFiles(HttpServletResponse response, Integer id) { List<ThreadAttachments> attachs = repo.findByMessageId(id); // 假设attachs包含文件名和绝对路径 try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) { for (ThreadAttachments atts : attachs) { Path path = Paths.get(atts.getPath(), atts.getNomeFile()); FileSystemResource fileSystemResource = new FileSystemResource(path); ZipEntry zipEntry = new ZipEntry(fileSystemResource.getFilename()); zipEntry.setSize(fileSystemResource.contentLength()); zipEntry.setTime(System.currentTimeMillis()); zipOutputStream.putNextEntry(zipEntry); StreamUtils.copy(fileSystemResource.getInputStream(), zipOutputStream); zipOutputStream.closeEntry(); } zipOutputStream.finish(); } catch (IOException e) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getLocalizedMessage()); } }
当数据库中存在文件路径和文件名记录,但对应文件不存在于文件系统时,会正确抛出IOException,但通过SwaggerUI或Postman测试时,仍能下载到空Zip文件。尝试了自定义异常结合@ControllerAdvice、将控制器代码放入try-catch块等方法,均无效。请问问题出在哪里?
问题原因与解决方案
核心原因
控制器方法开头就设置了Content-Type和Content-Disposition响应头,并且通过response.getOutputStream()创建了ZipOutputStream,此时响应的默认状态码已经是200,响应头已发送到客户端。后续即使抛出异常,Spring无法修改已发送的响应状态码或返回JSON格式的错误响应——客户端会认为这是正常的Zip文件下载,只是内容为空。
具体修复步骤
- 提前校验文件存在性
在创建ZipOutputStream之前,遍历所有记录检查文件是否真实存在,提前抛出异常(此时响应头未发送,Spring能正确返回错误响应):
public void zipFiles(HttpServletResponse response, Integer id) { List<ThreadAttachments> attachs = repo.findByMessageId(id); // 提前校验所有文件是否存在 for (ThreadAttachments atts : attachs) { Path path = Paths.get(atts.getPath(), atts.getNomeFile()); if (!Files.exists(path)) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "文件不存在: " + atts.getNomeFile()); } } // 后续执行原有打包逻辑 try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) { for (ThreadAttachments atts : attachs) { Path path = Paths.get(atts.getPath(), atts.getNomeFile()); FileSystemResource fileSystemResource = new FileSystemResource(path); ZipEntry zipEntry = new ZipEntry(fileSystemResource.getFilename()); zipEntry.setSize(fileSystemResource.contentLength()); zipEntry.setTime(System.currentTimeMillis()); zipOutputStream.putNextEntry(zipEntry); StreamUtils.copy(fileSystemResource.getInputStream(), zipOutputStream); zipOutputStream.closeEntry(); } zipOutputStream.finish(); } catch (IOException e) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getLocalizedMessage()); } }
- 调整控制器响应方式(更优雅)
放弃直接操作HttpServletResponse,返回ResponseEntity<Resource>让Spring统一处理响应:
@GetMapping("/getMessageAttachs/{id}") public ResponseEntity<Resource> getAttachmentsByMessage(@PathVariable("id") Integer id) throws IOException { List<ThreadAttachments> attachs = repo.findByMessageId(id); // 提前校验文件 for (ThreadAttachments atts : attachs) { Path path = Paths.get(atts.getPath(), atts.getNomeFile()); if (!Files.exists(path)) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "文件不存在: " + atts.getNomeFile()); } } // 将Zip内容写入字节数组流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zipOutputStream = new ZipOutputStream(baos)) { for (ThreadAttachments atts : attachs) { Path path = Paths.get(atts.getPath(), atts.getNomeFile()); FileSystemResource fileSystemResource = new FileSystemResource(path); ZipEntry zipEntry = new ZipEntry(fileSystemResource.getFilename()); zipEntry.setSize(fileSystemResource.contentLength()); zipEntry.setTime(System.currentTimeMillis()); zipOutputStream.putNextEntry(zipEntry); StreamUtils.copy(fileSystemResource.getInputStream(), zipOutputStream); zipOutputStream.closeEntry(); } zipOutputStream.finish(); } ByteArrayResource resource = new ByteArrayResource(baos.toByteArray()); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=files.zip") .body(resource); }
这种方式下,异常会被Spring异常处理器正确捕获,不会提前发送响应头,从而返回预期的错误响应。
- 响应重置(不推荐)
如果坚持直接操作HttpServletResponse,可尝试在抛出异常前重置响应,但仅当响应未提交时有效,可靠性低:
catch (IOException e) { try { response.reset(); // 仅在响应未提交时生效 } catch (IllegalStateException ex) { // 响应已提交,无法重置 } throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getLocalizedMessage()); }
内容的提问来源于stack exchange,提问作者CoderJammer
相关产品推荐
相关产品推荐

