Micronaut Problem JSON

Produce application/problem+json responses from a Micronaut application.

Version: 5.1.0

1 Introduction

Micronaut Problem is a library which makes it easy to produce application/problem+json responses from a Micronaut application. It connects the Problem library and Micronaut Error Formatting capabilities.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Breaking Changes

Micronaut Problem JSON 3.0.0

Starting with Micronaut Framework 4.0.0, Micronaut Problem JSON is based on jakarta.validation rather than javax.validation as in previous versions.

Micronaut Problem JSON 2.2.3

The default Problem+JSON payload does not include the detail field to avoid accidental information disclosure if the exception root cause is not of type UnsatisfiedRouteException or ThrowableProblem to avoid accidental information disclosure since 2.2.3.

You can customize it to include always the detail or for some scenarios.

4 Installation

implementation("io.micronaut.problem:micronaut-problem-json")
<dependency>
    <groupId>io.micronaut.problem</groupId>
    <artifactId>micronaut-problem-json</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.problem:micronaut-problem-json",
]

5 Usage

This library registers ProblemErrorResponseProcessor; an ErrorResponseProcessor for Problem. It sets the response content type to application/problem+json and the response HTTP Status code to match the status field for Problem.

Moreover, it registers ThrowableProblemHandler. A Micronaut ErrorHandler for handling ThrowableProblem exception.

5.1 Problem Builder

You can use Problem builder to create problem:

package io.micronaut.problem.docs;

import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Status;
import io.micronaut.problem.HttpStatusType;
import org.zalando.problem.Problem;

import java.net.URI;

@Controller("/product")
public class ProductController {
    @Get
    @Status(HttpStatus.OK)
    public void index() {
        throw Problem.builder()
                .withType(URI.create("https://example.org/out-of-stock"))
                .withTitle("Out of Stock")
                .withStatus(new HttpStatusType(HttpStatus.BAD_REQUEST))
                .withDetail("Item B00027Y5QG is no longer available")
                .with("product", "B00027Y5QG")
                .build();
    }
}
package io.micronaut.problem.docs

import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Status
import io.micronaut.problem.HttpStatusType
import org.zalando.problem.Problem
import java.net.URI

@Controller("/product")
class ProductController {
    @Get
    @Status(HttpStatus.OK)
    fun index() {
        throw Problem.builder()
                .withType(URI.create("https://example.org/out-of-stock"))
                .withTitle("Out of Stock")
                .withStatus(HttpStatusType(HttpStatus.BAD_REQUEST))
                .withDetail("Item B00027Y5QG is no longer available")
                .with("product", "B00027Y5QG")
                .build()
    }
}
package io.micronaut.problem.docs

import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Status
import io.micronaut.problem.HttpStatusType
import org.zalando.problem.Problem

@Controller("/product")
class ProductController {
    @Get
    @Status(HttpStatus.OK)
    void index() {
        throw Problem.builder()
                .withType(URI.create("https://example.org/out-of-stock"))
                .withTitle("Out of Stock")
                .withStatus(new HttpStatusType(HttpStatus.BAD_REQUEST))
                .withDetail("Item B00027Y5QG is no longer available")
                .with("product", "B00027Y5QG")
                .build()
    }
}
This example is Java, Kotlin and Groovy only. A Java Problem that Python builds and raises reaches the server carrying Truffle’s LazyStackTrace among its suppressed exceptions, which the problem+json body cannot serialize, so the client receives a 500 instead of the problem response (micronaut-core#13583). In Python, declare the problem as a class extending AbstractThrowableProblem and raise that, as shown in Custom Problem.

The above snippet returns:

{
    "status": 400,
    "title": "Out of Stock",
    "detail": "Item B00027Y5QG is no longer available",
    "type": "https://example.org/out-of-stock",
    "parameters": {"product": "B00027Y5QG"}
}

5.2 Custom Problem

You can create a custom problem by extending AbstractThrowableProblem

package io.micronaut.problem.docs;

import io.micronaut.http.HttpStatus;
import io.micronaut.problem.HttpStatusType;
import org.zalando.problem.AbstractThrowableProblem;

import java.net.URI;

public class TaskNotFoundProblem extends AbstractThrowableProblem {

    private static final URI TYPE = URI.create("https://example.org/not-found");

    public TaskNotFoundProblem(Long taskId) {
        super(TYPE, "Not found", new HttpStatusType(HttpStatus.NOT_FOUND), String.format("Task '%s' not found", taskId));
    }
}
from java.net import URI
from micronaut.http import HttpStatus
from micronaut.problem import HttpStatusType
from org.zalando.problem import AbstractThrowableProblem

TYPE = URI.create("https://example.org/not-found")


class TaskNotFoundProblem(AbstractThrowableProblem):

    def __init__(self, task_id: int):
        super().__init__(TYPE, "Not found", HttpStatusType(HttpStatus.NOT_FOUND), f"Task '{task_id}' not found")
package io.micronaut.problem.docs

import com.fasterxml.jackson.annotation.JsonIgnore
import io.micronaut.http.HttpStatus
import io.micronaut.problem.HttpStatusType
import io.micronaut.serde.annotation.Serdeable
import org.zalando.problem.AbstractThrowableProblem
import org.zalando.problem.Exceptional
import java.net.URI

@Serdeable
class TaskNotFoundProblem(taskId: Long) :
        AbstractThrowableProblem(URI.create("https://example.org/not-found"),
                "Not found",
                HttpStatusType(HttpStatus.NOT_FOUND),
                String.format("Task '%s' not found", taskId)) {

    @JsonIgnore
    override fun getCause(): Exceptional {
        TODO("Not yet implemented")
    }
}
package io.micronaut.problem.docs

import io.micronaut.http.HttpStatus
import io.micronaut.problem.HttpStatusType
import org.zalando.problem.AbstractThrowableProblem

class TaskNotFoundProblem extends AbstractThrowableProblem {

    private static final URI TYPE = URI.create("https://example.org/not-found")

    TaskNotFoundProblem(Long taskId) {
        super(TYPE, "Not found", new HttpStatusType(HttpStatus.NOT_FOUND), String.format("Task '%s' not found", taskId))
    }
}

If your logic throws such a Problem:

package io.micronaut.problem.docs;

import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.Status;

@Controller("/task")
public class TaskController {
    @Get("/{taskId}")
    @Status(HttpStatus.OK)
    public void index(@PathVariable Long taskId) {
        throw new TaskNotFoundProblem(taskId);
    }
}
from typing import Annotated

from micronaut.http import HttpStatus
from micronaut.http.annotation import Controller, Get, PathVariable, Status

from .TaskNotFoundProblem import TaskNotFoundProblem


@Controller("/task")
class TaskController:

    @Get("/{taskId}")
    @Status(HttpStatus.OK)
    def index(self, taskId: Annotated[int, PathVariable]) -> None:
        raise TaskNotFoundProblem(taskId)
package io.micronaut.problem.docs

import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable
import io.micronaut.http.annotation.Status

@Controller("/task")
class TaskController {
    @Get("/{taskId}")
    @Status(HttpStatus.OK)
    fun index(@PathVariable taskId: Long) {
        throw TaskNotFoundProblem(taskId)
    }
}
package io.micronaut.problem.docs

import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable
import io.micronaut.http.annotation.Status

@Controller('/task')
class TaskController {
    @Get('/{taskId}')
    @Status(HttpStatus.OK)
    void index(@PathVariable Long taskId) {
        throw new TaskNotFoundProblem(taskId)
    }
}

You will get:

{
    "status": 404,
    "title": "Not found",
    "detail": "Task '3' not found",
    "type": "https://example.org/not-found"
}

6 Configuration

You can configure it via:

🔗
Table 1. Configuration Properties for ProblemConfigurationProperties
Property Type Description Default value

problem.enabled

boolean

Sets whether the configuration is enabled. Default value true.

problem.stack-trace

boolean

Whether the HTTP Response should include the stack trace for instances of {@link org.zalando.problem.ThrowableProblem}. Default value (false).

7 Custom ProblemErrorResponseProcessor

The default Problem+JSON payload does not include the detail field to avoid accidental information disclosure if the exception root cause is not of type UnsatisfiedRouteException or ThrowableProblem.

You can extend ProblemJsonErrorResponseBodyProvider to customize the behaviour:

@Replaces(ProblemJsonErrorResponseBodyProvider.class)
@Singleton
public class ProblemErrorResponseProcessorReplacement
        extends ProblemJsonErrorResponseBodyProvider {
    ProblemErrorResponseProcessorReplacement(ProblemConfiguration config) {
        super(config);
    }

    @Override
    protected boolean includeErrorMessage(@NonNull ErrorContext errorContext) {
        return errorContext.getRootCause()
                .map(t -> t instanceof FooException || t instanceof UnsatisfiedRouteException)
                .orElse(false);
    }
}
@Replaces(ProblemJsonErrorResponseBodyProvider)
@Singleton
class ProblemErrorResponseProcessorReplacement(ProblemJsonErrorResponseBodyProvider):

    def __init__(self, config: ProblemConfiguration):
        super().__init__(config)

    def includeErrorMessage(self, error_context: ErrorContext) -> bool:
        return (error_context.getRootCause()
                .map(lambda t: isinstance(t, (FooException, UnsatisfiedRouteException)))
                .orElse(False))
@Replaces(ProblemJsonErrorResponseBodyProvider::class)
@Singleton
open class ProblemErrorResponseProcessorReplacement(config: ProblemConfiguration)
    : ProblemJsonErrorResponseBodyProvider(config) {

    override fun includeErrorMessage(errorContext: ErrorContext): Boolean {
        return errorContext.rootCause
            .map { t -> t is FooException || t is UnsatisfiedRouteException }
            .orElse(false)
    }
}
@Replaces(ProblemJsonErrorResponseBodyProvider)
@Singleton
class ProblemErrorResponseProcessorReplacement
        extends ProblemJsonErrorResponseBodyProvider {
    ProblemErrorResponseProcessorReplacement(ProblemConfiguration config) {
        super(config)
    }

    @Override
    protected boolean includeErrorMessage(@NonNull ErrorContext errorContext) {
        errorContext.rootCause
                .map(t -> t instanceof FooException || t instanceof UnsatisfiedRouteException)
                .orElse(false)
    }
}

When includeErrorMessage returns true for an unhandled exception, the detail contains the exception message (for example Internal Server Error: foo data), whatever the value of Micronaut’s micronaut.server.error-response-include-message setting.

8 Repository

You can find the source code of this project in this repository: