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

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文件下载,只是内容为空。

具体修复步骤

  1. 提前校验文件存在性
    在创建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());
    }
}
  1. 调整控制器响应方式(更优雅)
    放弃直接操作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异常处理器正确捕获,不会提前发送响应头,从而返回预期的错误响应。

  1. 响应重置(不推荐)
    如果坚持直接操作HttpServletResponse,可尝试在抛出异常前重置响应,但仅当响应未提交时有效,可靠性低:
catch (IOException e) {
    try {
        response.reset(); // 仅在响应未提交时生效
    } catch (IllegalStateException ex) {
        // 响应已提交,无法重置
    }
    throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getLocalizedMessage());
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 13:07:40