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, systemName and trace are filled in exactly as for a thrown ThrowableProblem.

level set on the problem — explicitly with withLevel(…​) or by default from the ProblemCode — is carried through to the response on both paths.