Spring Boot favicon.ico 404 Fix — The Catch-All Controller Trap
The Problem
You deploy your Spring Boot app to a server and /favicon.ico returns 404 — or worse, 500. Locally it works fine. You check the file path, confirm Spring Security permits it, and everything looks correct. Yet the server refuses to serve a simple .ico file.
If your project has a catch-all controller mapping like /**, that's likely your culprit.
What Was Happening
Here's the request flow I traced through the logs:
- Browser requests
/favicon.ico - Spring Security passes it through (confirmed by
Will secure Ant [pattern='/favicon.ico'] with []) - DispatcherServlet picks up the request
- The catch-all controller (
/**) matches before the static resource handler - Business logic runs — database queries for page templates, service config, popups
- Thymeleaf tries to resolve a template for "favicon.ico" and fails
- Error controller returns 404 or 500
The smoking gun was seeing business queries execute for a favicon request. Template lookups, service configuration queries — none of that should happen for a static file.
Why It Worked Locally
The app had a domain-based interceptor that identified the service context from the request's Host header. On localhost, it fell through to a default handler gracefully. On the deployed server with a real domain, the matching logic took a different path and broke.
The Root Cause
The catch-all controller hijacks /favicon.ico before Spring's static resource handler gets a chance. Even if the file exists in src/main/resources/static/, it doesn't matter — the controller mapping wins.
This is a handler mapping priority issue, not a missing file issue.
The Fix
Two changes, applied together:
1. Place the File in the Static Resource Path
Put favicon.ico in src/main/resources/static/ and verify it's included in the build artifact:
jar -tf app.jar | grep favicon
Check your build config for accidental excludes on .ico files.
2. Add a Filter to Short-Circuit the Request
This is the key fix. A OncePerRequestFilter at the highest priority intercepts /favicon.ico before it reaches any controller:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class FaviconFilter extends OncePerRequestFilter {
private byte[] faviconBytes;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if ("/favicon.ico".equals(request.getRequestURI())) {
if (faviconBytes == null) {
ClassPathResource res = new ClassPathResource("static/favicon.ico");
if (res.exists()) {
faviconBytes = StreamUtils.copyToByteArray(res.getInputStream());
} else {
response.sendError(404);
return;
}
}
response.setContentType("image/x-icon");
response.setHeader("Cache-Control", "max-age=604800");
response.setContentLength(faviconBytes.length);
response.getOutputStream().write(faviconBytes);
return; // Skip the entire filter chain
}
filterChain.doFilter(request, response);
}
}
Why a Filter instead of a dedicated @Controller? Because a controller still competes with the catch-all mapping for priority. Filters execute before the servlet layer entirely, so there's no ambiguity.
Other Files That Fall Into the Same Trap
If your app has a catch-all controller, robots.txt and sitemap.xml are likely affected too. You can extend the filter to handle all root-level static files, or serve them from Apache before the request ever reaches Tomcat:
Alias /favicon.ico /var/www/html/favicon.ico
ProxyPass /favicon.ico !
The Takeaway
When a static file returns 404 on a deployed server, don't just check if the file exists. Trace which handler is actually processing the request. The request lifecycle in Spring — Security → Filter → Interceptor → Controller — has multiple layers where things can go wrong. In this case, the file was there all along. The request just never reached the code that serves it.