如何阻止Spring根据Accept Header生成白标错误页面,始终返回JSON响应
我有一个生成JSON响应的Spring Boot REST API应用,控制器通过@ExceptionHandler结合response.sendError来处理自定义异常:
@ExceptionHandler(MyApiException.class) public void handleControllerException(MyApiException ex, HttpServletResponse response) throws IOException { response.sendError(ex.getStatus().value(), ex.getResponseMessage()); }
正常情况下会返回标准的JSON错误响应:
{"timestamp":"2018-01-30T11:22:33.456Z", "status":400, "error":"Bad Request", "message":"No customer with ID 123 found", "path":"/my/api/endpoint"}
但当客户端发送包含text/html的Accept头时(比如浏览器手动调试时),Spring会自动回退到HTML白标错误页面(纯文本展示如下):
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Jan 30 11:22:33 CET 2018
There was an unexpected error (type=Bad Request, status=400). No customer with ID 123 found
我希望禁用这个行为,让Spring始终返回JSON格式的错误响应。虽然找到了一些覆盖或禁用Spring错误页面的通用方法,但这些方法大多需要实现自定义错误页面,改动较大。这只是浏览器手动调试时的小问题(实际客户端不会发送这类Accept头),所以不想对应用做大改动,有没有简单的实现方式?
最简洁的方案:提前设置响应Content-Type
直接在调用sendError之前,强制把响应的Content-Type设为application/json,这样Spring的错误处理机制就会优先返回JSON格式,不会切换到HTML白标页面。只需要加一行代码,完全不需要额外配置:
@ExceptionHandler(MyApiException.class) public void handleControllerException(MyApiException ex, HttpServletResponse response) throws IOException { // 强制设置响应类型为JSON response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.sendError(ex.getStatus().value(), ex.getResponseMessage()); }
这个方法亲测有效,完美匹配你的需求——改动极小,只针对异常处理逻辑做微调,不会影响应用其他部分。
备选方案:手动构建JSON响应返回
如果你想完全绕过Spring的默认错误处理逻辑,可以自己构建符合格式的JSON响应体,直接写入输出流:
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.time.Instant; @ExceptionHandler(MyApiException.class) public void handleControllerException(MyApiException ex, HttpServletResponse response) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(ex.getStatus().value()); // 获取当前请求路径 String requestPath = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getRequest().getRequestURI(); // 构建和Spring默认格式一致的JSON响应 String jsonResponse = String.format( "{\"timestamp\":\"%s\", \"status\":%d, \"error\":\"%s\", \"message\":\"%s\", \"path\":\"%s\"}", Instant.now().toString(), ex.getStatus().value(), ex.getStatus().getReasonPhrase(), ex.getResponseMessage(), requestPath ); response.getWriter().write(jsonResponse); }
这种方式完全自主控制响应内容,不用担心Spring的视图解析逻辑干扰,但需要自己拼接JSON(或者用Jackson序列化一个错误DTO会更优雅)。
配置级方案:强制错误响应优先返回JSON
如果不想修改现有ExceptionHandler代码,可以通过配置Spring的错误视图解析器,让它始终返回JSON视图:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManagerFactoryBean; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpStatus; import java.util.Map; @Configuration public class ErrorResponseConfig { @Bean public ContentNegotiationManager contentNegotiationManager() { ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean(); // 设置默认响应类型为JSON factory.setDefaultContentType(MediaType.APPLICATION_JSON); factory.setFavorPathExtension(false); factory.addMediaType("json", MediaType.APPLICATION_JSON); return factory.getObject(); } @Bean public DefaultErrorViewResolver errorViewResolver(ContentNegotiationManager contentNegotiationManager) { return new DefaultErrorViewResolver() { @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 直接返回JSON视图,忽略HTML解析 ModelAndView mv = new ModelAndView(new MappingJackson2JsonView()); mv.addAllObjects(model); mv.setStatus(status); return mv; } }; } }
这个方案属于全局配置,一次设置后所有错误响应都会返回JSON,适合需要统一控制错误格式的场景。
内容的提问来源于stack exchange,提问作者l4mpi




