Spring Boot
custom 404 page
standalone Tomcat
Java
web development

Spring Boot - custom 404 page with standalone tomcat

Data Structures & Algorithms practice on Codemia

Step through 300+ coding interview problems with animated visualisers that show the data structure changing as the code runs. The Blind 75 are free.

Practice algorithms

Introduction

A custom 404 page is simple in embedded Spring Boot, but WAR deployment to standalone Tomcat adds extra moving parts. Static resources, template lookup, and container-level mappings can all affect what users see when a route is missing. The most reliable approach is to start with Spring Boot conventions, then add custom logic only where behavior must differ.

How Spring Boot Resolves 404 Responses

Spring Boot has an internal error flow that handles exceptions and missing routes. When the status is 404, it tries to resolve an error view from known locations. If that lookup fails, you usually get a default error response.

For an MVC app with Thymeleaf, use templates/error/404.html. For static HTML, use public/error/404.html.

text
1src/main/resources/
2  templates/
3    error/
4      404.html
5  public/
6    error/
7      404.html

In many projects, only one of these is needed. If you already render server templates, keep the page in templates/error/404.html so it can reuse your app layout.

WAR Packaging for Standalone Tomcat

The most common failure is not Java code. It is missing resources in the WAR. A local run can work while deployed Tomcat returns its own default 404 because your custom file was not packaged.

Check your artifact directly:

bash
./mvnw clean package
jar tf target/myapp.war | grep 'error/404'

If no output appears, your page is not inside the WAR. Review your build setup and resource includes. A minimal Maven setup that keeps resources intact looks like this:

xml
1<build>
2  <resources>
3    <resource>
4      <directory>src/main/resources</directory>
5    </resource>
6  </resources>
7</build>

Also verify your app is built as war and has the Spring Boot Tomcat dependency set correctly for external container deployment.

Configuration for a Predictable 404 Experience

Start with standard error properties. Disable the whitelabel response so your page or JSON body is always used.

properties
server.error.whitelabel.enabled=false
server.error.include-message=never
server.error.include-binding-errors=never

When you need finer behavior, add a resolver that only handles 404 and leaves other statuses untouched.

java
1package com.example.demo.config;
2
3import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
4import org.springframework.context.annotation.Bean;
5import org.springframework.context.annotation.Configuration;
6import org.springframework.http.HttpStatus;
7import org.springframework.web.servlet.ModelAndView;
8
9import jakarta.servlet.http.HttpServletRequest;
10import java.util.Map;
11
12@Configuration
13public class ErrorConfig {
14
15    @Bean
16    ErrorViewResolver notFoundViewResolver() {
17        return (HttpServletRequest request, HttpStatus status, Map<String, Object> model) -> {
18            if (status == HttpStatus.NOT_FOUND) {
19                return new ModelAndView("error/404", model);
20            }
21            return null;
22        };
23    }
24}

This keeps behavior explicit and avoids replacing handling for 500 responses accidentally.

Returning HTML for Browsers and JSON for APIs

Mixed applications often need two styles of 404 output. Browsers should get a readable page, while API clients should get structured JSON.

One practical pattern is to keep HTML view resolution for normal web routes and add a JSON handler for API paths.

java
1package com.example.demo.api;
2
3import org.springframework.http.ResponseEntity;
4import org.springframework.web.bind.annotation.ExceptionHandler;
5import org.springframework.web.bind.annotation.RestControllerAdvice;
6import org.springframework.web.servlet.NoHandlerFoundException;
7
8import java.util.Map;
9
10@RestControllerAdvice(basePackages = "com.example.demo.api")
11public class ApiErrorAdvice {
12
13    @ExceptionHandler(NoHandlerFoundException.class)
14    public ResponseEntity<Map<String, Object>> handleNoHandler(NoHandlerFoundException ex) {
15        return ResponseEntity.status(404).body(Map.of(
16                "error", "not_found",
17                "path", ex.getRequestURL(),
18                "message", "Requested API route does not exist"
19        ));
20    }
21}

If you use this pattern, enable throwing NoHandlerFoundException with the matching MVC settings in your application configuration.

Testing the Deployed Behavior

Unit tests are useful, but you also need at least one deploy-like check. Verify status code and response body type.

java
1package com.example.demo;
2
3import org.junit.jupiter.api.Test;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
6import org.springframework.boot.test.context.SpringBootTest;
7import org.springframework.test.web.servlet.MockMvc;
8
9import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
10import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
11import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
12
13@SpringBootTest
14@AutoConfigureMockMvc
15class NotFoundPageTest {
16
17    @Autowired
18    MockMvc mvc;
19
20    @Test
21    void unknownRouteReturns404Page() throws Exception {
22        mvc.perform(get("/no-such-page"))
23           .andExpect(status().isNotFound())
24           .andExpect(content().string(org.hamcrest.Matchers.containsString("Page not found")));
25    }
26}

After that, run a smoke test against Tomcat itself, not just local embedded mode.

Common Pitfalls

  • Custom page works in local run but not in Tomcat because error/404 files were excluded from WAR packaging. Confirm artifact contents before debugging controllers.
  • API clients receiving HTML 404 because only MVC page rendering was configured. Add API-specific JSON handling for missing routes.
  • Returning ModelAndView for every status in a resolver. Restrict custom logic to 404 so 500 diagnostics are not hidden.
  • Reverse proxy sends 404 before request reaches Spring app. Test both direct Tomcat endpoint and external ingress URL.
  • Relative links in the error page breaking under non-root context paths. Use context-aware links and verify assets load under deployed path.

Summary

  • Keep custom 404 handling convention-first with error/404 resources.
  • Validate WAR packaging early to avoid false code-level debugging.
  • Split browser and API error responses when serving both clients.
  • Limit custom resolver scope so non-404 errors remain visible and debuggable.
  • Add deploy-level tests to guarantee the same behavior in standalone Tomcat.

Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Data Structures & Algorithms practice on Codemia

Step through 300+ coding interview problems with animated visualisers that show the data structure changing as the code runs. The Blind 75 are free.

Practice algorithms