惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
MyScale Blog
MyScale Blog
雷峰网
雷峰网
博客园 - 叶小钗
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)
云风的 BLOG
云风的 BLOG
V
V2EX
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
N
Netflix TechBlog - Medium
Vercel News
Vercel News
美团技术团队
人人都是产品经理
人人都是产品经理
The Cloudflare Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Reusable Spring Boot 4 Error Handler
Eduardo Issa · 2026-05-08 · via DEV Community

Why use this library

Out of the box, Spring Boot returns a plain 500 Internal Server Error for most
unhandled exceptions, and its default BasicErrorController produces a flat JSON
object that varies between Spring versions and reveals internal details such as
exception class names and stack-trace snippets. This makes it hard for API clients
to handle errors programmatically and exposes information that should stay
server-side.

This library replaces that behaviour with a single, consistent error contract across
every failure mode your API can encounter:

  • Structured, machine-readable bodies. Every error follows RFC 9457 Problem Details with a stable code field (e.g. INVALID_ENUM_VALUE, VALIDATION_FAILED, DUPLICATE_VALUE). Clients can branch on code without parsing human-readable messages.
  • Precise field-level context. JSON deserialization errors include the exact JSONPath ($.address.street), the rejected value, the expected type, and — for enums — the full list of valid values. Validation errors list every constraint violation in one response so clients do not need to submit the form multiple times to discover all problems.
  • No internal leakage. Raw database messages, exception class names, and stack traces never reach the client. Constraint names are extracted from PostgreSQL error messages via regex and presented as opaque codes.
  • Consistent tracing. The Micrometer trace ID is stamped into every error body (traceId) and onto every HTTP response (X-Trace-Id header), so support teams can correlate a client-side error report directly to a distributed trace.
  • Drop-in auto-configuration. Package this module as a JAR and declare it as a dependency — no @Import, no @ComponentScan, no boilerplate. Spring Boot picks up the handlers automatically.

Requirements

Dependency Notes
Spring Boot 4.x Tested on 4.0.6
spring-boot-starter-webmvc Required
spring-boot-starter-validation Required for ValidationExceptionHandler
spring-boot-starter-data-jpa Required for DataExceptionHandler
spring-boot-starter-actuator + spring-boot-micrometer-tracing-brave + micrometer-tracing-bridge-brave Optional — enables traceId in bodies and X-Trace-Id header

Adding the library to a project

Copy the com.example.demo.error package into your project. Spring Boot's component
scan will pick up all @RestControllerAdvice and @Component beans automatically,
provided the package is within your application's scan root.

Packaging as a reusable JAR. If you extract these classes into a shared library,
register them in
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
(one fully-qualified class name per line). Spring Boot reads that file at startup and
activates the listed classes as auto-configurations, so consumers of the JAR need no
@Import or @ComponentScan — the dependency alone is sufficient.

What's included

JsonExceptionHandler — 400 Bad Request

Handles HttpMessageNotReadableException (Jackson deserialization failures). Maps the
Jackson 3 cause chain to a structured response:

Cause code Extra fields
UnrecognizedPropertyException UNKNOWN_JSON_FIELD path, validValues
InvalidFormatException (enum) INVALID_ENUM_VALUE path, invalidValue, expectedType, validValues
InvalidFormatException (scalar) INVALID_FIELD_VALUE path, invalidValue, expectedType
MismatchedInputException TYPE_MISMATCH path, expectedType
InputCoercionException INTEGER_OVERFLOW validRange, line, column
StreamReadException MALFORMED_JSON line, column
Other MALFORMED_REQUEST_BODY
{
  "type": "about:blank",
  "title": "Invalid enum value",
  "status": 400,
  "detail": "Cannot deserialize value 'UNKNOWN' as Category at path $.category — valid values: [ELECTRONICS, BOOKS, CLOTHING]",
  "instance": "/products",
  "code": "INVALID_ENUM_VALUE",
  "path": "$.category",
  "invalidValue": "UNKNOWN",
  "expectedType": "Category",
  "validValues": [
    "ELECTRONICS",
    "BOOKS",
    "CLOTHING"
  ],
  "traceId": "69fcf2db21f488679d633abb34871dbb"
}

Enter fullscreen mode Exit fullscreen mode

ValidationExceptionHandler — 422 Unprocessable Content

Handles MethodArgumentNotValidException (@Valid on @RequestBody) and
HandlerMethodValidationException (@Validated on individual parameters).

{
  "type": "about:blank",
  "title": "Validation failed",
  "status": 422,
  "detail": "One or more fields failed validation",
  "instance": "/products",
  "code": "VALIDATION_FAILED",
  "violations": [
    {
      "path": "$.name",
      "invalidValue": "",
      "message": "must not be blank"
    },
    {
      "path": "$.price",
      "invalidValue": "-1",
      "message": "must be greater than 0"
    }
  ],
  "traceId": "69fcf2db21f488679d633abb34871dbb"
}

Enter fullscreen mode Exit fullscreen mode

DataExceptionHandler — 404 / 409

Handles JPA and JDBC data exceptions. Constraint names are extracted from the PostgreSQL
error message via regex; raw database messages are never forwarded to the client.

Exception Condition code Status
DuplicateKeyException any DUPLICATE_VALUE 409
DataIntegrityViolationException unique constraint DUPLICATE_VALUE 409
DataIntegrityViolationException foreign key constraint REFERENTIAL_INTEGRITY_VIOLATION 409
DataIntegrityViolationException other DATA_INTEGRITY_VIOLATION 409
EmptyResultDataAccessException any RESOURCE_NOT_FOUND 404
EntityNotFoundException any RESOURCE_NOT_FOUND 404

GlobalExceptionHandler — Spring MVC infrastructure + catch-all

Extends ResponseEntityExceptionHandler to handle the full set of Spring MVC exceptions
(405, 415, 400, 404, 408, 413, …) and adds a stable code field to each. A final
@ExceptionHandler(Exception.class) returns 500 INTERNAL_SERVER_ERROR and logs
the stack trace at ERROR level.

ProblemDetailTraceAdvice

ResponseBodyAdvice that appends "traceId": "<hex>" to every ProblemDetail body
produced by an @ExceptionHandler. Scoped to exception handlers only — normal 2xx
responses are not intercepted.

TraceIdResponseHeaderFilter

OncePerRequestFilter that sets an X-Trace-Id: <hex> response header on every
response (success and error alike). Runs at Ordered.LOWEST_PRECEDENCE so it is
guaranteed to execute inside the ServerHttpObservationFilter span context.

Configuration

Jackson — unknown field detection

# Required for JsonExceptionHandler to produce UNKNOWN_JSON_FIELD responses.
# Without this, Jackson silently ignores unrecognised fields and the handler
# never sees an UnrecognizedPropertyException.
spring.jackson.deserialization.fail-on-unknown-properties=true

Enter fullscreen mode Exit fullscreen mode

Tracing

# Fraction of requests that receive a trace ID (0.0–1.0).
# Defaults to 0.1 (10 %). Set to 1.0 in development so every request
# gets an X-Trace-Id header and a traceId field in error bodies.
management.tracing.sampling.probability=1.0
# Disable tracing entirely without removing the dependency.
# ProblemDetailTraceAdvice and TraceIdResponseHeaderFilter become no-ops.
# management.tracing.enabled=false

Enter fullscreen mode Exit fullscreen mode

Logging

Log levels per handler

Each handler logs at a level that reflects the severity of the underlying problem.
Override individual loggers to tune verbosity:

# JsonExceptionHandler — DEBUG for all recognised causes, WARN for unclassified bodies
logging.level.com.example.demo.error.JsonExceptionHandler=DEBUG
# ValidationExceptionHandler — DEBUG for every violation set
logging.level.com.example.demo.error.ValidationExceptionHandler=DEBUG
# DataExceptionHandler — WARN for constraint violations, DEBUG for not-found
logging.level.com.example.demo.error.DataExceptionHandler=DEBUG
# GlobalExceptionHandler — ERROR for unexpected exceptions (stack trace included),
# everything else inherited from Spring's ResponseEntityExceptionHandler (WARN)
logging.level.com.example.demo.error.GlobalExceptionHandler=DEBUG
# Suppress the stack-trace log that Spring MVC itself emits for resolved exceptions.
# Useful in production to avoid double-logging when GlobalExceptionHandler already logs.
logging.level.org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler=ERROR

Enter fullscreen mode Exit fullscreen mode

Stack traces at TRACE level

JsonExceptionHandler and ValidationExceptionHandler deliberately split their log
output into two statements:

  • DEBUG — a one-line summary (field path, violation count, etc.) that is safe to enable in production without flooding logs.
  • TRACE — the full exception stack trace, logged separately via log.trace("Stack trace:", ex).

This means enabling DEBUG gives you actionable context for every bad request without
stack-trace noise. Enable TRACE only when you need to inspect the Jackson or Validator
internals (e.g. to diagnose a misconfigured deserializer):

# Stack traces for JSON deserialization failures
logging.level.com.example.demo.error.JsonExceptionHandler=TRACE
# Stack traces for Bean Validation failures
logging.level.com.example.demo.error.ValidationExceptionHandler=TRACE

Enter fullscreen mode Exit fullscreen mode

GlobalExceptionHandler.handleUnexpected is the exception to this pattern: it logs at
ERROR with the exception object directly (log.error("Unexpected error", ex)), so the
stack trace is always captured for genuinely unexpected failures regardless of the
configured level.

File upload size (for PAYLOAD_TOO_LARGE)

# Maximum size of a single uploaded file (default: 1MB)
spring.servlet.multipart.max-file-size=10MB
# Maximum size of the entire multipart request (default: 10MB)
spring.servlet.multipart.max-request-size=50MB

Enter fullscreen mode Exit fullscreen mode

When either limit is exceeded Spring throws MaxUploadSizeExceededException, which
GlobalExceptionHandler maps to 413 PAYLOAD_TOO_LARGE.

Internationalisation (i18n)

Every title and detail field in the error body is resolved through Spring's MessageSource,
with the locale read from LocaleContextHolder at exception-handling time. Translating error
messages to a new language requires only a properties file — no code changes.

Bundled locales

File Locale
messages.properties English (fallback)
messages_pt_BR.properties Portuguese — Brazil

Adding a locale

Create messages_<language>[_<COUNTRY>].properties alongside the existing files and translate
every key. Spring resolves the closest-matching bundle for the request locale automatically.

# src/main/resources/messages_es.properties
error.validation-failed.title=Validación fallida
error.validation-failed.detail=Uno o más campos no pasaron la validación
error.resource-not-found.title=Recurso no encontrado
error.resource-not-found.detail=El recurso solicitado no fue encontrado.
# … remaining keys …

Enter fullscreen mode Exit fullscreen mode

If you package this library as a JAR and your application defines its own messages.properties,
list both basenames so Spring merges them:

spring.messages.basename=messages,classpath:com/example/demo/error/messages
spring.messages.encoding=UTF-8

Enter fullscreen mode Exit fullscreen mode

Locale resolution

By default, Spring MVC's AcceptHeaderLocaleResolver maps the Accept-Language request header
to a java.util.Locale. When the header is absent or no bundle matches, Spring falls back to the
JVM default locale and then to the root messages.properties.

To resolve locale from a cookie or session instead, declare a LocaleResolver bean:


@Bean
LocaleResolver localeResolver() {
    var resolver = new CookieLocaleResolver("lang");
    resolver.setDefaultLocale(Locale.ENGLISH);
    return resolver;
}

Enter fullscreen mode Exit fullscreen mode

Validation constraint messages

The violations[].message field inside VALIDATION_FAILED responses comes from Bean
Validation
, not from the MessageSource above. To localise constraint messages, add a
ValidationMessages_<locale>.properties file to your classpath:

# src/main/resources/ValidationMessages_pt_BR.properties
jakarta.validation.constraints.NotBlank.message=não deve estar em branco
jakarta.validation.constraints.Size.message=o tamanho deve estar entre {min} e {max}

Enter fullscreen mode Exit fullscreen mode

Bean Validation resolves its bundle against the JVM default locale. To drive it from the
per-request locale, supply a locale-aware MessageInterpolator in your validator configuration.

Full dependency block for tracing support

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-micrometer-tracing-brave</artifactId>
  </dependency>
  <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-tracing-bridge-brave</artifactId>
  </dependency>

Enter fullscreen mode Exit fullscreen mode

Without these dependencies the traceId body field and X-Trace-Id header are simply
absent. All other error-handling behaviour is unaffected.

Error code reference

code Status Meaning
MALFORMED_JSON 400 Syntactically invalid JSON
MALFORMED_REQUEST_BODY 400 Body unreadable (unclassified)
UNKNOWN_JSON_FIELD 400 Field not declared on the target type
INVALID_ENUM_VALUE 400 Value not in enum constants
INVALID_FIELD_VALUE 400 Value cannot be coerced to target type
TYPE_MISMATCH 400 Wrong JSON token type for target
INTEGER_OVERFLOW 400 Numeric value outside type range
VALIDATION_FAILED 422 Bean Validation constraint failure
METHOD_VALIDATION_ERROR 422 Method-level validation failure
DUPLICATE_VALUE 409 Unique constraint violated
REFERENTIAL_INTEGRITY_VIOLATION 409 Foreign key constraint violated
DATA_INTEGRITY_VIOLATION 409 Other integrity constraint violated
RESOURCE_NOT_FOUND 404 Entity or query result not found
METHOD_NOT_ALLOWED 405 HTTP method not supported
UNSUPPORTED_MEDIA_TYPE 415 Content-Type not accepted
NOT_ACCEPTABLE 406 Requested Accept type unavailable
MISSING_PATH_VARIABLE 400 Required path variable absent
MISSING_REQUEST_PARAMETER 400 Required query parameter absent
MISSING_REQUEST_PART 400 Required multipart part absent
REQUEST_BINDING_ERROR 400 Servlet request binding failure
ROUTE_NOT_FOUND 404 No handler mapped for the request path
REQUEST_TIMEOUT 503 Async request timed out
PAYLOAD_TOO_LARGE 413 Upload exceeds configured limit
CONVERSION_NOT_SUPPORTED 500 No converter for property type
PARAMETER_TYPE_MISMATCH 400 Query/path parameter type coercion failed
MESSAGE_NOT_WRITABLE 500 Response body could not be serialised
INTERNAL_SERVER_ERROR 500 Unexpected exception

Source code

The source code of the this handlers is available in GitHub:
https://github.com/adzubla/springboot4-error-handler