==== Problem Details

Most APIs need a structured way to report errors, helping users understand exactly what went wrong. While you could invent a custom error-reporting format, it requires effort to design and forces your clients to learn a non-standard schema.

Instead, you can adopt the standard defined in https://www.rfc-editor.org/rfc/rfc7807[IETF RFC 7807] (later refined by https://www.rfc-editor.org/rfc/rfc9457[RFC 9457]). Adopting this standard saves you time and benefits your users by providing a familiar, widely supported error format.

Jooby provides built-in, native support for RFC 7807 Problem Details.

===== Setup

To enable Problem Details, simply add the following line to your configuration:

.application.conf
[source, properties]
----
problem.details.enabled = true
----

This minimal configuration enables a global error handler that catches all exceptions, transforms them into the Problem Details format, and renders the response based on the `Accept` header. It also sets the appropriate content type (e.g., `application/problem+json`).

You can customize the behavior using these additional properties:

.application.conf
[source, properties]
----
problem.details {
  enabled = true
  log4xxErrors = true                                // <1>
  muteCodes = [401, 403]                             // <2>
  muteTypes = ["com.example.MyMutedException"]       // <3>
}
----

<1> By default, only server errors (`5xx`) are logged. You can enable logging for client errors (`4xx`) as well. (If your logger is set to `DEBUG`, the log will also include the stack trace).
<2> Mute logging entirely for specific HTTP status codes.
<3> Mute logging entirely for specific Exception classes.

===== Creating Problems

The javadoc:problem.HttpProblem[] class represents the RFC 7807 model. Because it extends `RuntimeException`, you can throw it naturally just like any other exception.

====== Static Helpers

There are several static methods to quickly produce an javadoc:problem.HttpProblem[]:

* javadoc:problem.HttpProblem[valueOf, io.jooby.StatusCode]: Derives the title from the status code.
* javadoc:problem.HttpProblem[valueOf, io.jooby.StatusCode, java.lang.String]: Specifies a custom `title`.
* javadoc:problem.HttpProblem[valueOf, io.jooby.StatusCode, java.lang.String, java.lang.String]: Specifies a custom `title` and `detail`.

There are also shorthands for common HTTP errors:

* `HttpProblem.badRequest(...)` (400)
* `HttpProblem.notFound(...)` (404)
* `HttpProblem.unprocessableEntity(...)` (422)
* `HttpProblem.internalServerError()` (500)

.Throwing an HttpProblem
[source,java,role="primary"]
----
import io.jooby.problem.HttpProblem;
import io.jooby.StatusCode;

{
  get("/users/{userId}", ctx -> {
    String userId = ctx.path("userId").value();
    User user = userRepository.findUser(userId);

    if (user == null) {
      throw HttpProblem.valueOf(
        StatusCode.NOT_FOUND,
        "User Not Found",
        "User with ID " + userId + " was not found in the system."
      );
    }
    // ...
  });
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
import io.jooby.problem.HttpProblem
import io.jooby.StatusCode

{
  get("/users/{userId}") { ctx ->
    val userId = ctx.path("userId").value()
    val user = userRepository.findUser(userId)

    if (user == null) {
      throw HttpProblem.valueOf(
        StatusCode.NOT_FOUND,
        "User Not Found",
        "User with ID $userId was not found in the system."
      )
    }
    // ...
  }
}
----

**Resulting Response:**
[source,json]
----
{
    "timestamp": "2024-10-05T14:10:41.648933100Z",
    "type": "about:blank",
    "title": "User Not Found",
    "status": 404,
    "detail": "User with ID 123 was not found in the system.",
    "instance": null
}
----

====== Builder

For complex errors, use the builder to construct a rich problem instance with all standard properties:

.Using the Builder
[source,java,role="primary"]
----
throw HttpProblem.builder()
  .type(URI.create("http://example.com/invalid-params"))
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .detail("'Name' may not be empty")
  .instance(URI.create("http://example.com/invalid-params/3325"))
  .build();
----

.Kotlin
[source,kotlin,role="secondary"]
----
throw HttpProblem.builder()
  .type(URI.create("http://example.com/invalid-params"))
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .detail("'Name' may not be empty")
  .instance(URI.create("http://example.com/invalid-params/3325"))
  .build()
----

====== Extra Parameters

RFC 7807 allows you to add custom properties to the problem details object. To make serialization easier (especially in statically typed languages), Jooby groups all extra properties under a single root field called `parameters`.

You can add parameters via the builder:

.Adding Parameters
[source,java,role="primary"]
----
throw HttpProblem.builder()
  .title("Order not found")
  .status(StatusCode.NOT_FOUND)
  .detail("Order with ID " + orderId + " could not be processed.")
  .param("reason", "Order ID format incorrect or order does not exist.")
  .param("suggestion", "Please check the order ID and try again.")
  .param("supportReference", "/support")
  .build();
----

.Kotlin
[source,kotlin,role="secondary"]
----
throw HttpProblem.builder()
  .title("Order not found")
  .status(StatusCode.NOT_FOUND)
  .detail("Order with ID $orderId could not be processed.")
  .param("reason", "Order ID format incorrect or order does not exist.")
  .param("suggestion", "Please check the order ID and try again.")
  .param("supportReference", "/support")
  .build()
----

**Resulting Response:**
[source,json]
----
{
  "title": "Order not found",
  "status": 404,
  "detail": "Order with ID 123 could not be processed.",
  "parameters": {
    "reason": "Order ID format incorrect or order does not exist.",
    "suggestion": "Please check the order ID and try again.",
    "supportReference": "/support"
  }
}
----

====== Custom Headers

Some HTTP responses (like `413 Payload Too Large` or `426 Upgrade Required`) require specific response headers. You can append headers directly to your `HttpProblem`:

.Adding Headers
[source,java,role="primary"]
----
throw HttpProblem.builder()
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .header("my-string-header", "string-value")
  .header("my-int-header", 100)
  .build();
----

.Kotlin
[source,kotlin,role="secondary"]
----
throw HttpProblem.builder()
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .header("my-string-header", "string-value")
  .header("my-int-header", 100)
  .build()
----

====== Error Details (RFC 9457)

RFC 9457 introduced a standard way to deliver bulk validation errors via an `errors` array. You can add these using the `error()` or `errors()` methods in the builder:

.Adding Validation Errors
[source,java,role="primary"]
----
throw HttpProblem.builder()
  .title("Validation Failed")
  .status(StatusCode.BAD_REQUEST)
  .error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
  .error(new HttpProblem.Error("Last name is required", "/lastName"))
  .build();
----

.Kotlin
[source,kotlin,role="secondary"]
----
throw HttpProblem.builder()
  .title("Validation Failed")
  .status(StatusCode.BAD_REQUEST)
  .error(HttpProblem.Error("First name cannot be blank", "/firstName"))
  .error(HttpProblem.Error("Last name is required", "/lastName"))
  .build()
----

**Resulting Response:**
[source,json]
----
{
  "title": "Validation Failed",
  "status": 400,
  "errors": [
    {
      "detail": "First name cannot be blank",
      "pointer": "/firstName"
    },
    {
      "detail": "Last name is required",
      "pointer": "/lastName"
    }
  ]
}
----

[TIP]
====
If you need to enrich validation errors with more information, you can extend javadoc:problem.HttpProblem.Error[] to create your own custom error model.
====

====== Mapping Custom Exceptions

If your application already uses a suite of custom exception classes, you don't need to rewrite them. Make them Problem Details-compliant by implementing the javadoc:problem.HttpProblemMappable[] interface:

.Mapping Existing Exceptions
[source,java,role="primary"]
----
import io.jooby.problem.HttpProblemMappable;

public class MyBusinessException extends RuntimeException implements HttpProblemMappable {
    
  @Override
  public HttpProblem toHttpProblem() {
    return HttpProblem.builder()
      .title("Business Logic Violation")
      .status(StatusCode.CONFLICT)
      .detail(this.getMessage())
      .build();
  }
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
import io.jooby.problem.HttpProblemMappable

class MyBusinessException(message: String) : RuntimeException(message), HttpProblemMappable {
    
  override fun toHttpProblem(): HttpProblem {
    return HttpProblem.builder()
      .title("Business Logic Violation")
      .status(StatusCode.CONFLICT)
      .detail(this.message)
      .build()
  }
}
----

====== Custom Problem Types

You can easily define domain-specific problem types by extending `HttpProblem` and utilizing the builder:

.Custom Problem Class
[source,java,role="primary"]
----
public class OutOfStockProblem extends HttpProblem {

  private static final URI TYPE = URI.create("https://example.org/out-of-stock");

  public OutOfStockProblem(String product) {
    super(builder()
      .type(TYPE)
      .title("Out of Stock")
      .status(StatusCode.BAD_REQUEST)
      .detail("The product '" + product + "' is no longer available.")
      .param("suggestions", List.of("Grinder MX-17", "Grinder MX-25"))
      .build()
    );
  }
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
import java.net.URI

class OutOfStockProblem(product: String) : HttpProblem(
  builder()
    .type(URI.create("https://example.org/out-of-stock"))
    .title("Out of Stock")
    .status(StatusCode.BAD_REQUEST)
    .detail("The product '$product' is no longer available.")
    .param("suggestions", listOf("Grinder MX-17", "Grinder MX-25"))
    .build()
)
----

====== Custom Exception Handlers

The features above allow you to rely entirely on Jooby's built-in global error handler. However, if you have a niche use case that requires a custom exception handler, you can still catch the exception and manually delegate it to the Problem Details renderer:

.Custom Handler Delegation
[source,java,role="primary"]
----
{
  error(MyCustomException.class, (ctx, cause, code) -> {
    MyCustomException ex = (MyCustomException) cause;
    
    HttpProblem problem = HttpProblem.valueOf(code, ex.getMessage());  // <1>
    
    ctx.getRouter().getErrorHandler().apply(ctx, problem, code);       // <2>
  });
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
{
  error(MyCustomException::class) { ctx, cause, code ->
    val ex = cause as MyCustomException
    
    val problem = HttpProblem.valueOf(code, ex.message)                // <1>
    
    ctx.router.errorHandler.apply(ctx, problem, code)                  // <2>
  }
}
----

<1> Transform the custom exception into an `HttpProblem`.
<2> Propagate the problem back to the global `ProblemDetailsHandler` to ensure standard rendering.

[IMPORTANT]
====
**Do not attempt to render `HttpProblem` manually.** Because `HttpProblem` derives from `RuntimeException`, it contains a stack trace. If you render it manually without passing it to the global error handler, you risk accidentally exposing the raw stack trace to the client. Always propagate the problem back to the router's error handler so it can be safely sanitized and formatted.
====
