implementation("io.micronaut.problem:micronaut-problem-json")
Micronaut Problem JSON
Produce application/problem+json responses from a Micronaut application.
Version: 3.5.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.
It is necessary to specify application/problem+json
as an additional type for the JSON codec.
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
<dependency>
<groupId>io.micronaut.problem</groupId>
<artifactId>micronaut-problem-json</artifactId>
</dependency>
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;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Status;
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
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Status
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()
}
}
package io.micronaut.problem
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Status
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()
}
}
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;
import io.micronaut.http.HttpStatus;
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));
}
}
package io.micronaut.problem
import io.micronaut.http.HttpStatus
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))
}
}
package io.micronaut.problem
import com.fasterxml.jackson.annotation.JsonIgnore
import io.micronaut.http.HttpStatus
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")
}
}
If your logic throws such a Problem:
package io.micronaut.problem;
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);
}
}
package io.micronaut.problem
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)
}
}
package io.micronaut.problem
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)
}
}
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:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether the configuration is enabled. Default value true. |
|
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 ProblemErrorResponseProcessor to customize the behaviour:
@Replaces(ProblemErrorResponseProcessor.class)
@Singleton
public class ProblemErrorResponseProcessorReplacement
extends ProblemErrorResponseProcessor {
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);
}
}
8 Repository
You can find the source code of this project in this repository: