ZuploZuplo
LoginSign Up
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
    How Zuplo WorksRequest LifecycleProject StructureAuthenticationAPI KeysRate LimitingAPI ErrorsRoutingPolicy FundamentalsOpenAPIEnvironmentsSource Control and DeploymentDevelopment Options
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Concepts

API Errors

Well-designed API errors are as important as the successful responses your API returns. A good error response tells the caller what went wrong, whether the problem is on their side or yours, and what they can do about it. Zuplo encourages every API to return standard, actionable error messages so that developers integrating with your API spend less time guessing and more time building.

This page explains the error format Zuplo uses by default, how it shows up in the gateway, and how to customize the shape of error responses when your API has its own conventions.

Why standard errors matter

When every endpoint invents its own error shape, client code becomes brittle. Developers have to special-case each response, parse ad-hoc fields, and guess at whether a failure is retryable. Standardizing errors across your API produces three concrete benefits:

  • Faster integration -- consumers write one error handler that works everywhere.
  • Better observability -- logs, dashboards, and tools can parse errors consistently.
  • Clearer contracts -- your OpenAPI document can describe errors using the same schema for every operation.

A good error response is short, machine-readable, and specific. It identifies the kind of problem, says what happened in human terms, and includes enough context (a request ID, a field name, a retry hint) that the caller can take the next step without opening a support ticket.

The Problem Details format

Zuplo defaults to the Problem Details for HTTP APIs format defined by RFC 7807. Problem Details is a small, widely adopted JSON schema for representing errors from HTTP APIs. Responses use the application/problem+json content type and follow a consistent shape.

A typical Problem Details response from Zuplo looks like this:

Code
{ "type": "https://httpproblems.com/http-status/401", "title": "Unauthorized", "status": 401, "instance": "/v1/widgets", "trace": { "timestamp": "2026-04-19T17:13:31.352Z", "requestId": "28f2d802-8e27-49c8-970d-39d90ef0ac61", "buildId": "eb9ef87d-b55d-446e-9fdd-13c209c01b95" } }

The standard fields are:

  • type -- a URI that identifies the kind of problem. Every occurrence of the same problem should share the same type.
  • title -- a short, human-readable summary that should stay consistent for a given type.
  • status -- the HTTP status code, duplicated in the body so clients that log only the payload still see it.
  • detail -- a human-readable explanation of this particular occurrence. This is the field that varies from request to request.
  • instance -- a URI or path that identifies the specific request that produced the error.

Problem Details also allows extensions -- arbitrary additional fields that carry problem-specific data. Zuplo uses extensions to include a trace object containing the request ID, build ID, and timestamp on every error, which makes support requests easy to correlate with logs.

Keep title stable for a given error type and put request-specific information in detail or in extensions. Clients match on type and title; humans read detail.

How Zuplo uses Problem Details

Zuplo's built-in policies, handlers, and system errors all return Problem Details responses out of the box. When an inbound policy rejects a request -- for example, the API Key Authentication policy when a key is missing, or the Rate Limiting policy when a caller exceeds their quota -- the response body is a Problem Details object with a type, title, status, and trace. The same is true for system responses like unmatched routes and unsupported HTTP methods.

This means that consumers of a Zuplo-fronted API get a consistent error contract for free across gateway errors, even before you write any custom code.

Returning Problem Details from custom code

When you write a custom handler or a custom policy, return Problem Details responses using the HttpProblems helper from @zuplo/runtime. The helper has a method for every HTTP status code and automatically fills in type, title, status, instance, and trace.

Code
import { HttpProblems, ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { if (!request.user) { return HttpProblems.unauthorized(request, context); } return request; }

For the full list of methods and options, see the HttpProblems helper reference.

Adding context with detail and extensions

Override the default fields when you have something more useful to say. Use detail for a human-readable explanation of the specific failure, and use extension members for structured data that clients can act on.

Code
return HttpProblems.badRequest(request, context, { title: "Invalid value for query parameter 'take'", detail: "The take parameter must be a number less than 100. The provided value was 'hello'.", extensions: { parameter: "take", providedValue: "hello", }, });

The title stays consistent for every instance of this error, while detail and the parameter extension tell the caller exactly what to fix.

Throwing runtime errors

If your code throws rather than returning a response, use RuntimeError and ConfigurationError to attach structured context that the gateway can surface. Thrown errors are converted to Problem Details responses automatically, and any extensionMembers you attach flow through to the response body.

Code
import { RuntimeError } from "@zuplo/runtime"; throw new RuntimeError({ message: "Upstream database timed out", extensionMembers: { service: "orders-db", timeoutMs: 5000, }, });

See Runtime Errors for details on both error classes and patterns for mapping them to problem responses.

Customizing the error response format

Problem Details is the default, but it isn't the only option. If your API already has an established error schema, or if you want to wrap every error in a custom envelope, Zuplo provides two levels of customization.

Per-response overrides

The simplest customization is to override fields on individual responses. HttpProblems lets you change title, detail, type, instance, and add arbitrary extensions without giving up the standard format.

Code
return HttpProblems.tooManyRequests( request, context, { type: "https://errors.example.com/rate-limit-exceeded", detail: "You've exceeded the 1000 requests per hour plan limit.", extensions: { plan: "free", upgradeUrl: "https://example.com/upgrade", }, }, { "Retry-After": "3600", }, );

This approach keeps the Problem Details shape while letting you customize types, link to documentation, and include plan-specific or caller-specific metadata.

Formatting every error with ProblemResponseFormatter

For more control, use the ProblemResponseFormatter to build problem responses directly. This is useful when you want to compute the problem body yourself -- for example, mapping an upstream error payload into your own error taxonomy.

Code
import { ProblemResponseFormatter } from "@zuplo/runtime"; const problemDetails = { type: "https://errors.example.com/validation-failed", title: "Validation Failed", status: 400, detail: "The request body contains invalid fields.", instance: request.url, extensions: { code: "VAL_001", fields: ["email", "phone"], }, }; return ProblemResponseFormatter.format(problemDetails, request, context);

Replacing the error format globally

To change the shape of every error response the gateway returns -- including errors raised by built-in policies -- register a global error handler in your runtime extensions. The handler runs for any unhandled error in the pipeline and returns the response of your choice.

Code
import { RuntimeExtensions } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addErrorHandler(async (error, request, context) => { return new Response( JSON.stringify({ error: { code: "internal_error", message: error.message, requestId: context.requestId, }, }), { status: 500, headers: { "content-type": "application/json" }, }, ); }); }

Global error handlers let you keep Problem Details internally while projecting a different schema to your callers, or replace the format entirely if your API has an established error contract.

Replacing the default format means you also take responsibility for including trace information, preserving status codes, and documenting the new schema in your OpenAPI document. Most APIs are best served by customizing Problem Details fields rather than replacing the format.

Customizing specific system errors

Some gateway behaviors have dedicated extension points for error customization. For example, you can replace the default 404 response by registering a not-found handler, which is useful for serving custom error pages or matching your API's error schema on unmatched routes.

Choosing an approach

ScenarioApproach
Return a one-off error from a handler or policyHttpProblems with detail and extensions
Build problem responses from external error payloadsProblemResponseFormatter.format()
Attach context to thrown errorsRuntimeError with extensionMembers
Replace the error schema for every responseGlobal error handler via runtime.addErrorHandler
Customize only the 404 responseNot-found handler

Related resources

  • HttpProblems helper
  • ProblemResponseFormatter
  • Runtime Errors
  • Not-found Handler
  • Runtime Extensions
  • Custom Handlers
  • Policies
  • RFC 7807: Problem Details for HTTP APIs
Edit this page
Last modified on April 19, 2026
Rate LimitingRouting
On this page
  • Why standard errors matter
  • The Problem Details format
  • How Zuplo uses Problem Details
    • Returning Problem Details from custom code
    • Adding context with detail and extensions
    • Throwing runtime errors
  • Customizing the error response format
    • Per-response overrides
    • Formatting every error with ProblemResponseFormatter
    • Replacing the error format globally
    • Customizing specific system errors
  • Choosing an approach
  • Related resources
JSON
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript