A unified error format in Spring Boot takes one class: a @RestControllerAdvice that extends ResponseEntityExceptionHandler returns an RFC 9457 ProblemDetail for every case — validation errors, 404, 409 and unexpected 500s. The client always gets application/problem+json with a clear detail, while the stack trace stays in the log and is linked to the response through an errorId.
After this you will be able to
- plug in a
GlobalExceptionHandlerthat returns every API error asapplication/problem+json - return request body and parameter errors as an
errorslist that says where each error is - answer unexpected failures with a 500 without a stack trace and find them in the log by
errorId - check the response format with MockMvc tests in a
@WebMvcTestslice
Know beforehand
Verified with
Spring Boot 4.1.1Spring Framework 7.0.9Java 25 LTS (Eclipse Temurin 25.0.4)Hibernate Validator 9.1.3Maven 3.10.0
Checked against docs: October 12, 2026
Contents
The problem: API errors in one format
Without a shared handler every error looks different. An exception from a service becomes a 500 with Spring Boot's default body, a validation error becomes a 400 with its own fields, and a try/catch in a controller returns a plain string. The frontend and integrations have to parse several formats, and a stray ex.getMessage() in a response exposes a database address or a piece of SQL.
This recipe fixes it with one class. Every error response gets the application/problem+json type and the fields of RFC 9457 "Problem Details for HTTP APIs": type, title, status, detail, instance. In Spring Framework the standard is represented by the ProblemDetail class, and the ResponseEntityExceptionHandler base class already turns built-in Spring MVC errors into it. What each field means and when to define your own type is covered in the API design lesson.
Invalid request body fields
- Exception
MethodArgumentNotValidException- Response
- 400,
type…/validation-error, anerrorslist withpointer
A parameter constraint is violated (@Max on limit)
- Exception
HandlerMethodValidationException- Response
- 400, same
type, the parameter name inerrors
No such record
- Exception
NotFoundException(ours)- Response
- 404, no
typefield, which meansabout:blank
Conflict with existing data: SKU taken
- Exception
ConflictException(ours)- Response
- 409, our own
type…/duplicate-sku
Malformed JSON, wrong method, unknown URL
- Exception
- built-in Spring MVC exceptions
- Response
- 400, 405, 404 — handled by the base class
Anything else
- Exception
- any
Exception - Response
- 500, a generic text and an
errorId; the stack trace goes to the log only
Ready code: exceptions and GlobalExceptionHandler
First, two domain exceptions. The service throws them without knowing anything about HTTP: business logic does not depend on the web layer, and the status code is chosen in one place.
package com.example.shop.error;
// the resource does not exist: the handler turns this exception into a 404
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}package com.example.shop.error;
// the request conflicts with the current state of the data: 409
// type is a short problem-type name; the documentation URI is built from it
public class ConflictException extends RuntimeException {
private final String type;
private final String title;
public ConflictException(String type, String title, String detail) {
super(detail);
this.type = type;
this.title = title;
}
public String getType() {
return type;
}
public String getTitle() {
return title;
}
}Now the handler itself. It belongs in a package inside the application package: then @SpringBootApplication finds the class through component scanning and applies it to every controller.
package com.example.shop.error;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
// All API errors use the RFC 9457 format (application/problem+json).
// The base class already handles built-in Spring MVC errors:
// malformed JSON — 400, wrong method — 405, unknown URL — 404, and so on
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// documentation for the problem types should live at this address
static final String PROBLEMS = "https://api.example.com/problems/";
@ExceptionHandler(NotFoundException.class)
public ProblemDetail handleNotFound(NotFoundException ex) {
// no type set: per the RFC that means about:blank, and title becomes "Not Found"
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(ConflictException.class)
public ProblemDetail handleConflict(ConflictException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
problem.setType(URI.create(PROBLEMS + ex.getType()));
problem.setTitle(ex.getTitle());
return problem;
}
// Anything unexpected is a 500 without details. The stack trace goes only to the log,
// and the client gets an errorId that leads straight to that log entry
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnexpected(Exception ex, HttpServletRequest request) {
String errorId = UUID.randomUUID().toString();
log.error("Unhandled error {} in {} {}", errorId, request.getMethod(), request.getRequestURI(), ex);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
"Internal server error. Give support this code: " + errorId);
problem.setProperty("errorId", errorId);
return problem;
}
// @Valid @RequestBody: field errors in the request body
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
List<Map<String, String>> errors = new ArrayList<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.add(Map.of("pointer", toPointer(error.getField()), "detail", error.getDefaultMessage()));
}
return handleExceptionInternal(ex, validationProblem(status, errors), headers, status, request);
}
// constraints directly on method parameters: @RequestParam @Max(100) int limit
@Override
protected ResponseEntity<Object> handleHandlerMethodValidationException(
HandlerMethodValidationException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
List<Map<String, String>> errors = new ArrayList<>();
for (ParameterValidationResult result : ex.getParameterValidationResults()) {
String name = result.getMethodParameter().getParameterName();
for (MessageSourceResolvable error : result.getResolvableErrors()) {
errors.add(Map.of("parameter", name, "detail", error.getDefaultMessage()));
}
}
return handleExceptionInternal(ex, validationProblem(status, errors), headers, status, request);
}
private static ProblemDetail validationProblem(HttpStatusCode status, List<Map<String, String>> errors) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, "Check the request parameters");
problem.setType(URI.create(PROBLEMS + "validation-error"));
problem.setTitle("Validation failed");
problem.setProperty("errors", errors);
return problem;
}
// items[0].name -> #/items/0/name (JSON Pointer, as in the RFC 9457 example)
private static String toPointer(String field) {
return "#/" + field.replace("[", "/").replace("]", "").replace('.', '/');
}
}@RestControllerAdviceis@ControllerAdviceplus@ResponseBody: its handler methods apply to all controllers and write their result to the response body.- Extending
ResponseEntityExceptionHandlergives RFC 9457 handling for every built-in Spring MVC exception.handleMethodArgumentNotValidandhandleHandlerMethodValidationExceptionare overridden to add theerrorslist. - Validation errors arrive as two exceptions:
MethodArgumentNotValidExceptionfor an object with@Valid, andHandlerMethodValidationExceptionwhen constraints sit directly on method parameters. The Spring documentation recommends handling both. - The
Exception.classhandler does not take over built-in errors: among several matching methods Spring picks the one whose exception is closest to the thrown one in the hierarchy. So malformed JSON still gets a 400 from the base class, not a 500. - Spring fills in
instancewith the request path, and it does not writetypewhen it isabout:blank: under RFC 9457 a missing field means exactlyabout:blank. errorIdanderrorsare extension members. The RFC allows custom fields, and clients must ignore the ones they do not recognise.
How to plug it in
Dependencies
You need
spring-boot-starter-webmvcandspring-boot-starter-validation, plusspring-boot-starter-webmvc-testfor tests. A start.spring.io project with thewebandvalidationdependencies already has them.Copy three classes
NotFoundException,ConflictExceptionandGlobalExceptionHandlergo into anerrorpackage. Replacehttps://api.example.com/problems/with the address where your error types will be documented: RFC 9457 recommends that a type URI lead to a human-readable description.Throw exceptions from the service
Instead of
return nullorResponseEntity.notFound()in the controller, the service throwsNotFoundExceptionorConflictException. The controller stays free oftry/catch.Rewrite built-in error texts
By default Spring answers an unknown URL with the
detail"No static resource api/unknown." — an unnecessary hint about the internals. Texts are set inmessages.propertiesunder the keyproblemDetail.plus the fully qualified exception class name.No need for the problemdetails property
spring.mvc.problemdetails.enabled=trueturns on Spring Boot's built-in handler only when there is noResponseEntityExceptionHandlerbean of your own. With the class from this recipe you do not need the property.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- … -->
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>The service and controller the recipe was tested on: in-memory storage, a unique SKU and a limit constraint placed directly on the method parameter.
package com.example.shop.product;
import java.math.BigDecimal;
public record Product(long id, String name, String sku, BigDecimal price) {
}package com.example.shop.product;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import java.math.BigDecimal;
public record ProductRequest(
@NotBlank(message = "Name is required")
String name,
@NotBlank(message = "SKU is required")
@Pattern(regexp = "[A-Z0-9-]{3,20}", message = "SKU must be 3 to 20 characters: A–Z, 0–9 and hyphen")
String sku,
@NotNull(message = "Price is required")
@Positive(message = "Price must be greater than zero")
BigDecimal price) {
}package com.example.shop.product;
import com.example.shop.error.ConflictException;
import com.example.shop.error.NotFoundException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Service;
// in-memory storage, so the recipe does not depend on a database
@Service
public class ProductService {
private final Map<Long, Product> products = new ConcurrentHashMap<>();
private final Map<String, Long> idsBySku = new ConcurrentHashMap<>();
private final AtomicLong sequence = new AtomicLong();
public List<Product> list(int limit) {
return products.values().stream().limit(limit).toList();
}
public Product get(long id) {
Product product = products.get(id);
if (product == null) {
throw new NotFoundException("Product " + id + " not found");
}
return product;
}
public Product create(ProductRequest request) {
long id = sequence.incrementAndGet();
if (idsBySku.putIfAbsent(request.sku(), id) != null) {
throw new ConflictException("duplicate-sku", "SKU already taken",
"A product with SKU " + request.sku() + " already exists");
}
Product product = new Product(id, request.name().strip(), request.sku(), request.price());
products.put(id, product);
return product;
}
}package com.example.shop.product;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
// no try/catch in the controller: GlobalExceptionHandler deals with errors
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public List<Product> list(
@RequestParam(defaultValue = "20")
@Min(value = 1, message = "Must be at least 1")
@Max(value = 100, message = "Must be at most 100") int limit) {
return service.list(limit);
}
@GetMapping("/{id}")
public Product get(@PathVariable long id) {
return service.get(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product create(@Valid @RequestBody ProductRequest request) {
return service.create(request);
}
}# texts for built-in Spring MVC errors: do not reveal internals
problemDetail.org.springframework.web.servlet.resource.NoResourceFoundException=No such URL in this API
problemDetail.org.springframework.http.converter.HttpMessageNotReadableException=The request body is not valid JSON
problemDetail.org.springframework.beans.TypeMismatchException=Parameter "{0}" has an invalid formatmessages.properties as UTF-8 and wires it into the handler automaticallyHow to check: MockMvc tests and responses
@WebMvcTest starts only the web layer — controllers, @ControllerAdvice, JSON converters — without services or a database. We replace the service with @MockitoBean and make it throw the exceptions we need. Spring Boot's OutputCaptureExtension captures the output so we can check that the errorId from the response reached the log together with the stack trace.
package com.example.shop.error;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.example.shop.product.ProductController;
import com.example.shop.product.ProductService;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
// web slice: controller + GlobalExceptionHandler, the service is replaced with a mock
@WebMvcTest(ProductController.class)
@ExtendWith(OutputCaptureExtension.class)
class GlobalExceptionHandlerTest {
@Autowired
MockMvc mvc;
@MockitoBean
ProductService service;
@Test
void fieldErrorsBecome400WithPointers() throws Exception {
mvc.perform(post("/api/products").contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": " ", "sku": "abc", "price": -5}
"""))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.type").value("https://api.example.com/problems/validation-error"))
.andExpect(jsonPath("$.errors", hasSize(3)))
.andExpect(jsonPath("$.errors[?(@.pointer == '#/name')].detail").value("Name is required"))
.andExpect(jsonPath("$.errors[?(@.pointer == '#/price')].detail").value("Price must be greater than zero"));
}
@Test
void parameterErrorsBecome400() throws Exception {
mvc.perform(get("/api/products").param("limit", "500"))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.errors[0].parameter").value("limit"))
.andExpect(jsonPath("$.errors[0].detail").value("Must be at most 100"));
}
@Test
void notFoundBecomes404() throws Exception {
given(service.get(42)).willThrow(new NotFoundException("Product 42 not found"));
mvc.perform(get("/api/products/42"))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.type").doesNotExist()) // no field means about:blank
.andExpect(jsonPath("$.title").value("Not Found"))
.andExpect(jsonPath("$.detail").value("Product 42 not found"))
.andExpect(jsonPath("$.instance").value("/api/products/42"));
}
@Test
void unknownUrlIsAlsoProblemJson() throws Exception {
mvc.perform(get("/api/unknown"))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.detail").value("No such URL in this API")); // from messages.properties
}
@Test
void conflictBecomes409WithOwnType() throws Exception {
given(service.create(any())).willThrow(
new ConflictException("duplicate-sku", "SKU already taken", "A product with SKU CUP-01 already exists"));
mvc.perform(post("/api/products").contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Mug", "sku": "CUP-01", "price": 2500}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.type").value("https://api.example.com/problems/duplicate-sku"))
.andExpect(jsonPath("$.title").value("SKU already taken"));
}
@Test
void brokenJsonDoesNotEchoParserDetails() throws Exception {
mvc.perform(post("/api/products").contentType(MediaType.APPLICATION_JSON).content("{oops"))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.detail").value("The request body is not valid JSON"));
}
@Test
void unexpectedErrorBecomes500WithoutLeak(CapturedOutput output) throws Exception {
given(service.get(7)).willThrow(new IllegalStateException("connect to db-prod.internal:5432 failed"));
String body = mvc.perform(get("/api/products/7"))
.andExpect(status().isInternalServerError())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.errorId").value(matchesPattern("[0-9a-f-]{36}")))
.andExpect(content().string(not(containsString("db-prod"))))
.andExpect(content().string(not(containsString("IllegalStateException"))))
.andReturn().getResponse().getContentAsString();
// the same errorId is in the log, next to the exception and its stack trace
String errorId = JsonPath.read(body, "$.errorId");
assertThat(output).contains(errorId).contains("IllegalStateException: connect to db-prod.internal:5432 failed");
}
}$ docker run --rm -v "$PWD:/app" -v "$HOME/.m2:/root/.m2" -w /app maven:3.10-eclipse-temurin-25 mvn -B package
…
[INFO] Running com.example.shop.error.GlobalExceptionHandlerTest
…
… ERROR … c.e.shop.error.GlobalExceptionHandler : Unhandled error e6c77069-6ef4-495a-a7cd-6c060fb14c20 in GET /api/products/7
java.lang.IllegalStateException: connect to db-prod.internal:5432 failed
at com.example.shop.product.ProductService.get(ProductService.java:24) ~[classes/:na]
…
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 24.82 s -- in com.example.shop.error.GlobalExceptionHandlerTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.422 s -- in com.example.shop.ShopApplicationTests
…
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
…
[INFO] BUILD SUCCESSThe validation error response from the test run (formatted for reading). The order of errors items can change between runs, so the test looks an item up by pointer, not by index:
{
"detail": "Check the request parameters",
"instance": "/api/products",
"status": 400,
"title": "Validation failed",
"type": "https://api.example.com/problems/validation-error",
"errors": [
{
"pointer": "#/sku",
"detail": "SKU must be 3 to 20 characters: A–Z, 0–9 and hyphen"
},
{
"pointer": "#/name",
"detail": "Name is required"
},
{
"pointer": "#/price",
"detail": "Price must be greater than zero"
}
]
}{
"detail": "Internal server error. Give support this code: 51ba9062-b34e-495f-8c6b-38468fd347c5",
"instance": "/api/products/7",
"status": 500,
"title": "Internal Server Error",
"errorId": "51ba9062-b34e-495f-8c6b-38468fd347c5"
}On the running application (java -jar target/shop-0.0.1-SNAPSHOT.jar) the real service produces the same format: creating a product with the same SKU again returns a 409.
$ curl -s -X POST localhost:8080/api/products -H "Content-Type: application/json" -d '{"name": "Mug", "sku": "CUP-01", "price": 2500}'
{"id":1,"name":"Mug","sku":"CUP-01","price":2500}
$ curl -s -i -X POST localhost:8080/api/products -H "Content-Type: application/json" -d '{"name": "Mug", "sku": "CUP-01", "price": 2500}'
HTTP/1.1 409
Content-Type: application/problem+json
Transfer-Encoding: chunked
…
{"detail":"A product with SKU CUP-01 already exists","instance":"/api/products","status":409,"title":"SKU already taken","type":"https://api.example.com/problems/duplicate-sku"}What not to send to the client, and limitations
- Stack traces, class names and
ex.getMessage()of unexpected errors: the RFC 9457 security section warns against exposing implementation details, and OWASP advises returning a generic message and logging the details on the server - SQL text, table names, internal service addresses and library versions
- Other people's personal data in
detail— for example, who owns an email address that is already taken - Spring's default technical texts: replace them in
messages.properties
- Errors thrown before the
DispatcherServlet— for example, in Spring Security filters — never reach@RestControllerAdvice: the@ExceptionHandlermechanism works at theDispatcherServletlevel. Spring Boot's standard/errorproduces those responses, and they need separate configuration to match. - The
Exception.classhandler will also catch exceptions that are meant to be handled further down the chain, such asAccessDeniedExceptionfrom@PreAuthorize: you get a 500 instead of a 403. Add a dedicated@ExceptionHandlerfor such types or rethrow the exception — the Spring documentation describes how a rethrown exception propagates through the rest of the resolver chain. - Checking SKU uniqueness in memory is a simplification. With a database, a unique index guarantees uniqueness, and a constraint violation must be turned into a 409 in the same way, without the database error text.
errorIdis the minimal link between a response and the log. If tracing is enabled in the project, Spring Boot writes a correlation ID to the log, and that is the better value to hand to the client.- The UI should show users its own texts chosen by
typeand status code. RFC 9457 advises against parsingdetailprogrammatically, and forabout:blankSpring takestitlefrom the English status phrase — "Not Found".
Links and next step
- "Spring Boot: a REST API from scratch" — a project with JPA, validation and tests that this handler drops into unchanged.
- "API design: resources, versions, errors, pagination" — which problem types to define and how to describe the error format in an OpenAPI contract.
- "Error handling: try/catch, async errors and clear messages" — where to catch errors and how a user-facing message differs from a log entry.
- Official sources are listed below: the Spring Framework "Error Responses" section, RFC 9457 and the OWASP error handling cheat sheet.
How this was verified5 · passedchecked 12 October 2026
“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.