How Spring Boot Handles Errors: From /error to Custom Responses
This article explains how Spring Boot 2.4.11 processes exceptions, routes them to the default /error endpoint, and uses BasicErrorController along with ContentNegotiatingViewResolver and StaticView to generate HTML or JSON error responses based on the Accept header, including code examples and configuration details.
Environment
Spring Boot 2.4.11
Configuration
The following demonstration uses the interface shown below.
<code>@RestController
@RequestMapping("/exceptions")
public class ExceptionsController {
@GetMapping("/index")
public Object index(int a) {
if (a == 0) {
throw new BusinessException();
}
return "exception";
}
}
</code>Default Error Display
By default, when an exception occurs Spring MVC forwards the request to /error . The response format depends on the Accept header, yielding either HTML or JSON.
Error Response Mechanism
Spring MVC’s DispatcherServlet catches exceptions and eventually forwards them to the BasicErrorController at /error .
<code>public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(...) {
try {
// process result and check for exceptions
processDispatchResult(...);
}
}
private void processDispatchResult(...) {
if (exception != null) {
// resolve exception via HandlerExceptionResolver chain
ModelAndView mv = processHandlerException(...);
errorView = (mv != null);
}
}
protected ModelAndView processHandlerException(...) {
// default Rest API has no generic HandlerExceptionResolver
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
ModelAndView exMv = resolver.resolveException(...);
if (exMv != null) {
break;
}
}
// if none resolves, rethrow the exception
throw ex;
}
}
</code>The BasicErrorController defines two methods to handle different Accept header values.
<code>@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(
getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
</code>When Accept: text/html ,
errorHtmlreturns a ModelAndView that is later rendered by DispatcherServlet .
<code>if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
String viewName = mv.getViewName();
View view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
</code>The ContentNegotiatingViewResolver selects the appropriate view based on the requested media types.
<code>public View resolveViewName(String viewName, Locale locale) throws Exception {
List<MediaType> requestedMediaTypes = getMediaTypes(request);
if (requestedMediaTypes != null) {
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
// ... fallback handling
}
</code>The candidate views include ErrorMvcAutoConfiguration$StaticView and InternalResourceView . The resolver ultimately chooses StaticView for HTML responses.
When logging is set to
trace, the selected view is logged, confirming that StaticView is used.
The StaticView renders a simple HTML page containing timestamp, error message, status, and optional stack trace.
<code>public class StaticView implements View {
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
builder.append("<html><body><h1>Whitelabel Error Page</h1>")
.append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div>" + model.get("timestamp") + "</div>")
.append("<div>There was an unexpected error (type=" + htmlEscape(model.get("error")) + ", status=" + htmlEscape(model.get("status")) + ").</div>");
if (model.get("message") != null) {
builder.append("<div>" + htmlEscape(model.get("message")) + "</div>");
}
if (model.get("trace") != null) {
builder.append("<div style='white-space:pre-wrap;'>" + htmlEscape(model.get("trace")) + "</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
}
</code>Understanding this flow helps you customize error handling, replace the default whitelabel page, or provide richer JSON error bodies for API clients.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.