Vue3 + Spring Boot 文件下载异常错误友好提示处理
问题描述
在进行文件下载功能开发时,当前端请求后端接口返回业务异常(如模板文件不存在)时,前端仅显示通用的'文件下载失败'提示,无法展示具体的业务错误信息(如'模板文件不存在')。
原因分析
- 前端响应类型:前端 Axios 请求设置了
responseType: 'blob',导致后端返回的 JSON 格式错误信息被当作二进制流处理。 - 后端返回类型冲突:Controller 方法返回类型为
ResponseEntity<Resource>,成功时返回文件流,但异常时若直接返回 JSON 对象会导致类型不匹配或前端无法正确解析。 - 编码问题:错误文件响应未明确指定 UTF-8 字符集,导致中文乱码。
解决方案
1. 后端优化:全局异常处理器
使用 @RestControllerAdvice 统一处理异常。针对文件下载请求和普通请求返回不同格式的响应,确保错误信息可被前端识别。
GlobalExceptionHandler.java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(TemplateNotFoundException.class)
public Object handleTemplateNotFoundException(TemplateNotFoundException e, HttpServletRequest request) {
log.error("模板文件不存在:{}", e.getMessage(), e);
if (ErrorFileResponseUtils.isFileDownloadRequest(request)) {
return ErrorFileResponseUtils.createJsonErrorResponse("模板文件不存在", HttpStatus.NOT_FOUND);
}
return Result.error("模板文件不存在");
}
@ExceptionHandler(ResourceNotFoundException.class)
public Object handleResourceNotFoundException(ResourceNotFoundException e, HttpServletRequest request) {
log.error("资源文件不存在:{}", e.getMessage(), e);
if (ErrorFileResponseUtils.isFileDownloadRequest(request)) {
return ErrorFileResponseUtils.createJsonErrorResponse("文件不存在", HttpStatus.NOT_FOUND);
}
return Result.error("文件不存在");
}
@ExceptionHandler(Exception.class)
public Object handleException(Exception e, HttpServletRequest request) {
log.error("系统异常:{}", e.getMessage(), e);
if (ErrorFileResponseUtils.isFileDownloadRequest(request)) {
return ErrorFileResponseUtils.createJsonErrorResponse("操作失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
return Result.error("操作失败");
}
}
ErrorFileResponseUtils.java
确保错误响应包含正确的 Content-Type 和字符集。
public static ResponseEntity<Result<Object>> createJsonErrorResponse(String message, HttpStatus status) {
Result<Object> result = Result.error(message);
return ResponseEntity
.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(result);
}
public static boolean isFileDownloadRequest(HttpServletRequest request) {
String path = request.getRequestURI();
if (path.contains("/export") || path.contains("/download")) {
return true;
}
String acceptHeader = request.getHeader(HttpHeaders.ACCEPT);
return acceptHeader != null && acceptHeader.contains(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
2. 前端优化:Axios 拦截器
修改 Axios 响应拦截器,在捕获 blob 错误时尝试解析内容。优先解析 JSON,若失败则提取文本内容。
src/utils/request.ts
import axios from "axios";
import { ElMessage } from "element-plus";
const instance = axios.create({ baseURL: "/api" });
instance.interceptors.response.use(
async (response) => {
// 正常逻辑...
return response.data;
},
async (error) => {
// 401 未授权
if (error.response?.status === 401) {
router.push("/login");
return Promise.reject(error);
}
// 处理文件下载的错误响应
if (
error.config?.responseType === "blob" &&
error.response?.data instanceof Blob &&
error.response?.data.size < 1024
) {
try {
const errorText = await error.response.data.text();
try {
const errorJson = JSON.parse(errorText);
const message = errorJson.message || errorJson.msg || "文件下载失败";
ElMessage.error(message);
} catch {
// 非 JSON 格式,尝试提取核心错误信息
let message = errorText;
const match = errorText.match(/^\[.*?\]\s*(.*)/);
if (match && match[1]) {
message = match[1];
}
ElMessage.error(message || "文件下载失败");
}
} catch {
ElMessage.error("文件下载失败");
}
return Promise.reject(error);
}
// 普通错误响应
const errorData = error.response?.data || {};
ElMessage.error(errorData.msg || errorData.message || "服务异常!");
return Promise.reject(error);
}
);
export default instance;
3. 控制器代码调整
移除 Controller 中的 try-catch,直接抛出异常由全局处理器处理。
CapitalInfoController.java
@PostMapping("/export-data-by-template")
public ResponseEntity<Resource> exportDataByTemplate(@RequestBody @Valid CapitalInfoQueryDTO queryDTO) throws IOException {
String filePath = capitalInfoService.exportFileByTemplate(queryDTO);
Path path = Paths.get(filePath);
Resource resource = new UrlResource(path.toUri());
if (!resource.exists()) {
throw new ResourceNotFoundException("资源文件不存在");
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "attachment;filename=" + URLEncoder.encode(FileUtils.getFileName(filePath), StandardCharsets.UTF_8))
.body(resource);
}
总结
通过统一后端异常响应格式并增强前端对 Blob 错误内容的解析能力,实现了文件下载场景下业务异常的精准提示,同时解决了中文乱码问题。
