Skill85 repo starsupdated 6d ago
problem-details-rfc9457
This Spring Boot 3.x skill demonstrates implementing RFC 9457 Problem Details standard for REST API error responses. It provides a global exception handler configuration using ProblemDetail that formats validation failures, business rule violations, and not-found errors into standardized JSON responses with type URIs, titles, status codes, and custom properties. Use this when building production REST APIs requiring consistent, machine-readable error reporting across different exception types.
Install in Claude Code
Copygit clone --depth 1 https://github.com/rrezartprebreza/spring-boot-skills /tmp/problem-details-rfc9457 && cp -r /tmp/problem-details-rfc9457/skills/problem-details-rfc9457 ~/.claude/skills/problem-details-rfc9457Then start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Problem Details — RFC 9457
Spring Boot 3.x includes native RFC 9457 support via `ProblemDetail`.
## Enable in application.yml
```yaml
spring:
mvc:
problemdetails:
enabled: true # enables Spring's built-in RFC 9457 handler
```
## ProblemDetail Response Shape
```json
{
"type": "https://api.example.com/errors/order-not-found",
"title": "Order Not Found",
"status": 404,
"detail": "No order found with id: 550e8400-e29b-41d4-a716-446655440000",
"instance": "/api/v1/orders/550e8400-e29b-41d4-a716-446655440000",
"errorCode": "ORDER_NOT_FOUND",
"timestamp": "2026-04-13T10:00:00Z"
}
```
## Global Exception Handler
```java
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ProblemDetail handleNotFound(EntityNotFoundException ex, HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setType(URI.create("https://api.example.com/errors/not-found"));
problem.setTitle("Resource Not Found");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", "NOT_FOUND");
problem.setProperty("timestamp", Instant.now());
return problem;
}
@ExceptionHandler(BusinessRuleViolationException.class)
public ProblemDetail handleBusinessRule(BusinessRuleViolationException ex, HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex.getMessage());
problem.setType(URI.create("https://api.example.com/errors/business-rule"));
problem.setTitle("Business Rule Violation");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", ex.getErrorCode());
problem.setProperty("timestamp", Instant.now());
return problem;
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request
) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Request validation failed");
problem.setType(URI.create("https://api.example.com/errors/validation"));
problem.setTitle("Validation Failed");
problem.setProperty("timestamp", Instant.now());
problem.setProperty("violations", ex.getBindingResult().getFieldErrors().stream()
.map(e -> Map.of("field", e.getField(), "message", e.getDefaultMessage()))
.toList());
return ResponseEntity.badRequest().body(problem);
}
@ExceptionHandler(Exception.class)
public ProblemDetail handleGeneric(Exception ex, HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred");
problem.setType(URI.create("https://api.example.com/errors/internal"));
problem.setTitle("Internal Server Error");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("timestamp", Instant.now());
// Don't expose ex.getMessage() in production — log it instead
log.error("Unhandled exception at {}", request.getRequestURI(), ex);
return problem;
}
}
```
## Custom Domain Exceptions
```java
// Base exception
public abstract class DomainException extends RuntimeException {
private final String errorCode;
protected DomainException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// Specific exceptions
public class OrderNotFoundException extends DomainException {
public OrderNotFoundException(UUID orderId) {
super("ORDER_NOT_FOUND", "Order not found: " + orderId);
}
}
public class InsufficientInventoryException extends DomainException {
public InsufficientInventoryException(UUID productId, int requested, int available) {
super("INSUFFICIENT_INVENTORY",
"Insufficient inventory for product %s: requested %d, available %d"
.formatted(productId, requested, available));
}
}
```
## Content Negotiation
Clients can request problem details explicitly with `Accept: application/problem+json`. Spring handles this automatically when `problemdetails.enabled: true` — your handler returns `ProblemDetail` and Spring sets the correct `Content-Type: application/problem+json`.
## ProblemDetail vs ApiResponse Envelope
| Use Case | Approach |
|---|---|
| Error responses only | ProblemDetail (RFC 9457) |
| All responses (success + error) | ApiResponse envelope |
| Public API with diverse clients | ProblemDetail — RFC standard |
| Internal microservices | Either — be consistent across services |
Choose one approach per project. Don't mix `ApiResponse` for success and `ProblemDetail` for errors — it confuses API consumers who see two different shapes.
## Gotchas
- Agent assumes `problemdetails.enabled: true` covers everything — it only converts *Spring's own* MVC exceptions; your domain exceptions still need `@ExceptionHandler` methods
- Agent returns `Map<String, Object>` for errors — use `ProblemDetail`
- Agent exposes raw exception messages in 500 errors — log it, return generic message
- Agent uses custom error envelope alongside ProblemDetail — pick one standard
- Agent forgets to set `type` URI — required for RFC 9457 compliance
- Agent returns 200 with error in body — always use the correct HTTP status code
- Agent creates handler without extending `ResponseEntityExceptionHandler` — extend it to get Spring's built-in exception handling for free