Development Utilities
Published on: May 6, 2026
10 min read

Stop Returning {success:false}: RFC 9457 Problem Details for Modern APIs

✍️ By Priya Singh (Principal Software Engineer)

Principal Software Engineer

Stop Returning {success:false}: RFC 9457 Problem Details for Modern APIs
By Priya SinghSenior Technical Insights
Try the Tool

Ready to put this into practice?

We've built a high-performance REST API Tester specifically for the topics discussed in this article. It's free, secure, and runs entirely in your browser.

RFC 9457 Problem Details is an IETF standard that defines a consistent, machine-readable JSON format for HTTP API error responses − content type application/problem+json, with standard fields for type, title, status, detail, and instance. Spring Boot 3.x supports it natively through the ProblemDetail class. If your API still returns {"success": false}, you are maintaining a bespoke error contract that every SDK client, frontend, and monitoring tool has to special-case − when the standard already exists and your framework already ships it.

Every team I work with has invented their own error format. Some return {"success": false, "message": "..."}. Some return {"error": {"code": 404, "description": "..."}}. Some return {"errorCode": "USR_NOT_FOUND"}. Some return all three depending on which engineer wrote the endpoint. The result is the same: clients cannot handle errors consistently, monitoring cannot parse them generically, and new consumers have to read the docs just to understand what went wrong.

RFC 9457 solves this. Spring Boot 3.x ships it. Here is how to use it.

Why Custom Error JSON Breaks SDKs, Clients, and Monitoring

The "we have our own error format" decision feels harmless at sprint one. It becomes a hidden maintenance tax on every consumer as the API grows.

⚠️
What I found in a real integration

A payment gateway API returned validation errors as {"errors": [{"field": "amount", "msg": "invalid"}]}, authentication failures as {"code": 401, "reason": "token_expired"}, and rate limits as {"status": "throttled"} − all with HTTP 200. Three different shapes, same status code, twelve consuming teams. Every team had written separate error-handling logic. None of it was consistent.

🚫 Common Production Anti-Pattern − Stop Doing This
Java · the shapes I find across a single API codebase
// Endpoint A − validation error
return ResponseEntity.ok(
    Map.of("success", false, "errors", List.of("email is invalid"))
);  // HTTP 200. Clients cannot retry. Monitoring shows green.

// Endpoint B − auth failure
return ResponseEntity.ok(
    Map.of("code", 401, "reason", "token_expired")
);  // Still HTTP 200. Frontend has no idea what to do.

// Endpoint C − not found
return ResponseEntity.status(404)
    .body(Map.of("errorCode", "USR_NOT_FOUND", "description", "No user with that ID"));
// Different shape, different field names, no Content-Type signal.

The cost in practice:

  • SDK clients must implement custom deserialization for each error shape. A field rename is a breaking change − even if the HTTP status and business logic are identical.
  • Frontend frameworks − Axios interceptors, React Query error handlers, Angular HTTP interceptors − are built around consistent error structures. Custom formats accumulate into per-endpoint exception handling that becomes unmaintainable.
  • API gateways can parse and transform standard error responses. They cannot generically transform bespoke JSON objects.
  • Monitoring tools can extract and index error detail from predictable fields. When every endpoint has different field names, error analysis requires raw JSON string parsing instead of structured field queries.

Why HTTP Status Codes Alone Are Not Enough

Status codes are necessary. They are not sufficient. A 422 Unprocessable Entity tells the client that validation failed. It does not say which field failed, why it failed, or what to change. A 409 Conflict signals a state collision − but not whether the conflict is a duplicate email, an optimistic lock version mismatch, or a business rule violation requiring a completely different resolution path.

The Two-Layer Error Contract

Clients need both: the machine-readable status code that drives control flow, and structured error detail that drives the response. RFC 9457 is the bridge − standardising the detail layer on top of the status code layer.

💡
IETF Standard

RFC 9457 supersedes RFC 7807 (2016) and is the current IETF standard for Problem Details for HTTP APIs.

What RFC 9457 Defines

The standard is intentionally minimal. Five fields, one required, rest recommended or optional, fully extensible with your own fields.

FieldTypeRequiredPurpose
typeURI stringRequiredURI identifying the problem type. Use about:blank when no specific type applies.
titleStringRecommendedShort human-readable summary.
statusIntegerRecommendedThe HTTP status code. Must match the HTTP response status exactly.
detailStringOptionalHuman-readable explanation specific to this occurrence.
instanceURI stringOptionalURI identifying this specific occurrence for log correlation.
extensionsAnyExtensibleAdd fields like errors[], traceId, or retryAfter.

Content-Type: application/problem+json

Error responses following RFC 9457 must use Content-Type: application/problem+json. This allows clients to identify a structured error response without reading the body.

✗ Custom format (today)

// HTTP 200
{
  "success": false,
  "errors": [
    { "field": "email", "msg": "invalid" }
  ]
}

✓ RFC 9457 (consistent)

// HTTP 422
{
  "type": "https://api.co/problems/validation",
  "title": "Validation Failed",
  "status": 422,
  "detail": "One or more fields invalid.",
  "errors": [
    { "field": "email", "message": "invalid" }
  ]
}

Validation Errors − Field-Level Detail as RFC 9457 Extension

One of the most frequent questions I get in workshops is: "Where do the field-level errors go if they aren't in the standard?". RFC 9457 is intentionally minimal. It defines the core wrapper but encourages the use of Extension Members for problem-specific details. For validation, the industry standard has converged on adding an errors array of objects.

💡
Why 422 Unprocessable Entity?

While many legacy APIs use 400 Bad Request for everything, senior developers prefer 422 for validation. It signals that the request was syntactically correct (valid JSON/XML) but semantically invalid (invalid field values). This distinction allows clients to separate "You sent me garbage I can't parse" from "I parsed your data, but your email is invalid."

Here is how a production-grade validation extension looks. Notice we include a pointer or field and a machine-readable reason alongside the human message:

{
  "type": "https://api.yourdomain.com/probs/validation-error",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The request contained 2 validation errors",
  "instance": "/logs/abc-123",
  "errors": [
    {
      "field": "email",
      "reason": "INVALID_FORMAT",
      "message": "must be a valid email address"
    },
    {
      "field": "age",
      "reason": "MIN_VALUE",
      "message": "must be 18 or older"
    }
  ]
}
Senior Pro Tip: Using a reason code (like INVALID_FORMAT) allows your frontend to handle internationalization (i18n) locally. Instead of relying on the backend's English string, the frontend looks up the translation key for INVALID_FORMAT.

Spring Boot 3 − Enable RFC 9457 in One Line

application.properties
# Enable ProblemDetail for built-in Spring MVC exceptions
spring.mvc.problemdetails.enabled=true

That single line gives you RFC 9457 responses for all Spring MVC built-in exceptions automatically. For your own exceptions − validation failures, auth errors, business logic exceptions − you need the global handler below.

The Copy-Paste Global Exception Handler

Java · Spring Boot 3 · @RestControllerAdvice
@RestControllerAdvice
public class GlobalProblemHandler {
    private static final String BASE = "https://api.yourdomain.com/problems/";

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException ex) {
        ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.UNPROCESSABLE_ENTITY);
        pd.setType(URI.create(BASE + "validation-failed"));
        pd.setTitle("Validation Failed");
        pd.setProperty("errors", ex.getBindingResult().getFieldErrors());
        return ResponseEntity.status(422).body(pd);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ProblemDetail> handleUnexpected(Exception ex) {
        ProblemDetail pd = ProblemDetail.forStatus(500);
        pd.setTitle("Internal Server Error");
        return ResponseEntity.status(500).body(pd);
    }
}
What this gives you

Every error path − validation, auth, not-found − returns application/problem+json with a consistent structure and correct HTTP status code. Clients write one error-handling function for the entire API.

JWT Auth Failures − Differentiating Expired, Invalid, and Missing

When a client receives a 401 Unauthorized, the immediate question for the frontend is: "Do I need to login again, or is my token simply expired and needs refreshing?". Bespoke error formats often force the client to parse string messages like "Token is expired", which is fragile and error-prone.

RFC 9457 allows you to use distinct type URIs to communicate these states machine-readably, enabling your authorization interceptors to act decisively.

✗ Ambiguous 401

{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Token expired"
}

✓ Distinct Problem Types

{
  "type": "https://api.yourdomain.com/probs/token-expired",
  "title": "Token Expired",
  "status": 401,
  "detail": "Credential expired at 2026-05-01T12:00:00Z",
  "expiredAt": "2026-05-01T12:00:00Z"
}

This allows your Axios/Fetch interceptors to branch logic based on the type field. If the type is token-expired, you can automatically trigger a POST /auth/refresh call; if it's invalid-token or missing-token, you redirect to the login page immediately. No string parsing required.

⚠️
Don't Forget the Header

RFC 9110 requires a 401 response to include a WWW-Authenticate header. Combine this with RFC 9457 to provide both the protocol-level requirement and the application-level detail.

Downstream Service Failures − 503 with Retry Guidance

In microservices, your API is often a orchestrator. When a downstream service (like a payment gateway, database, or a third-party legacy system) is unavailable, returning a generic 500 is an operational lie. It implies your code crashed and alerts your on-call engineer for the wrong reason.

Returning a 503 Service Unavailable with a retryAfter extension member tells the client and the infrastructure exactly how to handle the transient failure.

{
  "type": "https://api.yourdomain.com/probs/service-unavailable",
  "title": "Service Unavailable",
  "status": 503,
  "detail": "The Payment Provider is undergoing scheduled maintenance.",
  "retryAfter": 30,
  "downstreamService": "Stripe"
}

By including retryAfter as an extension (and ideally as a Retry-After HTTP header), you allow automated retry libraries like Resilience4j or Polly to wait for the specified window. This prevents the "thundering herd" effect where thousands of clients retry immediately after a blip, crashing your system just as it tries to recover.

Operational Visibility

When your monitoring shows a spike in 503s specifically with type service-unavailable, you know the issue is external capacity or maintenance. If you see 500s, you know you have a regression in your own code to fix.

Security Considerations − Avoiding Information Leakage

While RFC 9457 encourages detail, be extremely careful not to leak internal implementation details in the detail or extension fields. In production, never include:

  • Java/Node stack traces
  • SQL fragments or database constraint names
  • Internal server hostnames or IP addresses
  • Library version numbers
The Pro Pattern

Only include technical detail in the instance field via a Trace ID. The user sees a friendly message, but the engineer can use that ID to find the full stack trace in the logs.

Business Rule Violations − Using 409 Conflict for State Collisions

When a request is valid (422 passed) and the user is authorized (401/403 passed), but the operation cannot complete due to the current state of the resource, use 409 Conflict. This is common for duplicate records or optimistic locking failures.

{
  "type": "https://api.yourdomain.com/probs/email-exists",
  "title": "Email Already Registered",
  "status": 409,
  "detail": "The email p.singh@example.com is already linked to an account."
}

Instance ID − Precise Log Correlation

The instance field is often overlooked but critical for production troubleshooting. It should contain a URI that uniquely identifies this specific error occurrence. In a microservices environment, this is usually your traceId.

💡
Debugging at 2am

When a user reports an error, they can provide the instance ID from the JSON response. You can then paste this ID directly into your log aggregator (Datadog, Splunk, or CloudWatch) to find the exact stack trace, request headers, and downstream calls associated with that specific failure.

One Standard. Every Error. No Exceptions.

RFC 9457 eliminates complexity by providing a predictable contract. Five fields. A content type. A URI per problem class. Spring Boot 3.x ships the implementation.

Stop returning {"success": false}. Ship the standard. − Priya

Test Your Error Responses

Trigger validation failures and verify the status code and RFC 9457 detail structure before a consumer finds the inconsistency.

Related on LearnHubly − Security Content Cluster
Third-Party Links Disclaimer

This article may contain links to third-party websites, documentation, tools, or services for reference and additional information. These external resources are maintained by their respective owners, and LearnHubly does not control or guarantee their availability, accuracy, security, or content. Please review the terms and privacy policies of third-party websites before using their services.

Priya Singh

Java
Spring Boot
React
APIs

Principal Software Engineer • 15+ Years Experience

Priya Singh is a Principal Software Engineer with 15+ years of experience building scalable applications and developer tools. She specializes in backend architecture, APIs, and performance optimization.