Usage
To signal a problem from a controller, build a Problem, attach any contextual info, and throw it:
@Controller("/pets")
public class PetController {
Map<String, Pet> petRepository = new ConcurrentHashMap<>();
@Get("/{name}")
public Pet findPetByName(@PathVariable String name) {
return petRepository.computeIfAbsent(
name,
(missing) -> {
throw Problem.of(HttpStatus.NOT_FOUND.getCode(), PetProblemCode.PetNotFound)
.withInfo("name", missing)
.toThrowable();
}
);
}
}
With the Micronaut integration in place, the thrown ThrowableProblem is translated into an application/problem+json HTTP response automatically — there’s no extra controller-level error handling to write.
The code and detail come from the ProblemCode enum referenced above.
See Defining Problem Codes for how to define one.
Adding context
Use withInfo(…) to attach the data that explains this specific occurrence of the problem:
throw Problem.of(HttpStatus.BAD_REQUEST.getCode(), PetProblemCode.InvalidName)
.withInfo("name", suppliedName)
.withInfo("reason", "must be lowercase")
.toThrowable();
The info map is serialized as part of the response body, so callers can act on it programmatically.
Custom exception handlers
Sometimes the exception that signals a problem is not a ThrowableProblem — a domain exception, a persistence error, a failed upstream call.
A Micronaut ExceptionHandler translates it, and the temptation is to return HttpResponse.badRequest(problem) directly.
Don’t: that bypasses the library, so the response is served as application/json, the HTTP status can disagree with the body’s status, and title, systemName and trace are missing.
Inject ProblemResponseFactory and return toResponse(problem) instead:
package no.conta.problem.micronaut.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;
import no.conta.problem.Problem;
import no.conta.problem.micronaut.ProblemResponseFactory;
@Produces
@Singleton
public class PetAlreadyExistsExceptionHandler implements ExceptionHandler<PetAlreadyExistsException, HttpResponse<Problem>> {
private final ProblemResponseFactory responseFactory;
public PetAlreadyExistsExceptionHandler(ProblemResponseFactory responseFactory) {
this.responseFactory = responseFactory;
}
@Override
public HttpResponse<Problem> handle(HttpRequest request, PetAlreadyExistsException exception) {
return responseFactory.toResponse(
Problem.of(PetProblemCode.PetAlreadyExists)
.withInfo("name", exception.getName())
);
}
}
The factory guarantees the same contract as problems the library renders itself:
-
the HTTP status is the problem’s status (falling back to the configured default), so envelope and body can never disagree,
-
the content type is always
application/problem+json, -
title,systemNameandtraceare filled in exactly as for a thrownThrowableProblem.
level set on the problem — explicitly with withLevel(…) or by default from the ProblemCode — is carried through to the response on both paths.