Error Handling
commons-rest treats errors as part of the API contract. You throw a typed
RuntimeException; auto-configured handlers render it as an RFC 9457 problem
details response with content type
application/problem+json.
The exceptions
Section titled “The exceptions”The exceptions live in commons-rest-api and can be thrown from anywhere — controller,
service, converter:
| Exception | Status | |
|---|---|---|
NotFoundException |
404 | resource does not exist |
BadRequestException |
400 | invalid input beyond bean validation |
InsufficientPrivilegesException |
403 | authenticated but not allowed |
NotFoundException is the workhorse: Spring has no simple “throw and get a problem+json 404”
exception of its own, so this fills the gap — perfect for orElseThrow:
CompanyEntity entity = repository.findById(id) .orElseThrow(NotFoundException::new);Each exception has three constructors — empty, message, or a full ErrorResponse:
throw new NotFoundException();throw new NotFoundException("employee not found");throw new NotFoundException(ErrorResponse.builder() .detail("employee not found") .instance("/api/employee/4711") .build());Checks bean validation can’t express
Section titled “Checks bean validation can’t express”Reach for BadRequestException when a rule needs code — a uniqueness check, a state
transition, a cross-field rule with repository access. Thanks to the fields map the client
receives the error in exactly the same shape as a bean-validation
failure, so the frontend handles both identically:
if (repository.existsByEmail(write.getEmail())) { throw new BadRequestException(ErrorResponse.builder() .detail("invalid form") .addField("email", "already taken") .build());}For simple annotation-expressible rules, prefer bean validation — it documents itself in the OpenAPI doc and feeds the generated Zod schemas.
The problem details body
Section titled “The problem details body”ErrorResponse follows RFC 9457 — @JsonInclude(NON_NULL), so unset members are omitted:
| Member | Type | Meaning |
|---|---|---|
type |
string (URI) | problem type, e.g. urn:problem-type:form-error; clients assume about:blank when absent |
title |
string | short summary, defaults to the HTTP reason phrase |
status |
int | HTTP status code |
detail |
string | human-readable explanation of this occurrence |
instance |
string (URI) | which resource/request the problem occurred on |
fields |
map | extension member: field path → list of validation messages |
{ "type": "urn:problem-type:form-error", "title": "Bad Request", "status": 400, "detail": "invalid form", "fields": { "email": ["must not be empty"] }}Handy helpers on the DTO: addField(path, message), hasField(path),
getFirstFieldValue(path).
Bean validation
Section titled “Bean validation”A failing @Valid / @Validated request body is handled by the
BeanValidationExceptionHandler — no code needed on your side. Every field error lands in the
fields map; messages are translated through your MessageSource using the key
error.form.<constraint> (e.g. error.form.NotNull) with the constraint’s default message as
fallback.
@PostMapping("/company")CompanyRead create(@RequestBody @NotNull @Valid CompanyWrite write);{ "type": "urn:problem-type:form-error", "title": "Bad Request", "status": 400, "detail": "invalid form", "fields": { "email": ["must be a well-formed email address", "must not be null"] }}The locale is resolved per request via the auto-configured AcceptHeaderLocaleResolver — see
i18n.
Let Spring speak problem+json too
Section titled “Let Spring speak problem+json too”The handlers above cover your exceptions. For Spring’s own errors — unknown route, 405, malformed JSON — enable Spring’s built-in problem details support so the whole API speaks one error language:
spring: mvc: problemdetails: enabled: trueClient side: RestTemplate
Section titled “Client side: RestTemplate”When consuming a service that speaks this error format, register the
BasicResponseErrorHandler — it converts 400/404/403 responses back into the same typed
exceptions, with the parsed ErrorResponse attached:
RestTemplate restTemplate = new RestTemplate();restTemplate.setErrorHandler(new BasicResponseErrorHandler());
try { restTemplate.getForObject(url, CompanyRead.class);} catch (NotFoundException e) { ErrorResponse body = e.getErrorResponse();}Switching handlers off
Section titled “Switching handlers off”Every handler bean is @ConditionalOnMissingBean — define your own and it wins. Or disable
per property:
handler: notFound.enabled: true badRequest.enabled: true beanValidation.enabled: true insufficientPrivileges.enabled: true