How to Handle Errors in 9router API Routes: Patterns and Best Practices
Use the centralized error utilities in open-sse/utils/error.js to return OpenAI-compatible JSON error responses, normalize upstream provider failures with parseUpstreamError, and signal rate-limit conditions via unavailableResponse with retry headers.
The decolua/9router repository implements a consistent, standards-compliant error-handling strategy across all server-side API endpoints. This approach ensures that every route returns predictable JSON error objects while internal logic manages retries, logging, and provider-specific failure translation.
Centralized Error Utilities
All route handlers in 9router rely on the helper functions exported from open-sse/utils/error.js. This module standardizes error formatting to match the OpenAI API schema, ensuring compatibility with existing client SDKs and downstream services.
Core Error Helper Functions
The module provides six primary exports that cover every error scenario:
buildErrorBody(statusCode, message)– Returns a plain object{ error: { message, type, code } }conforming to the OpenAI error schema.errorResponse(statusCode, message)– WrapsbuildErrorBodyin an HTTPResponsewith appropriateContent-Typeand CORS headers.unavailableResponse(statusCode, message, retryAfter, retryAfterHuman)– Used when every credential for a provider is rate-limited; adds aRetry-Afterheader to the response.parseUpstreamError(response, executor?)– Normalizes error payloads from upstream providers (OpenAI, Claude, Gemini, etc.), falling back to generic messages when provider formats differ.createErrorResult(statusCode, message, resetsAtMs?)– Returns a structured result object{ success:false, status, error, response, … }that core handlers return directly.formatProviderError(error, provider, model, statusCode)– Produces human-readable log strings that include low-level causes likeECONNRESETwithout exposing sensitive credentials.
Source: [open-sse/utils/error.js](https://github.com/decolua/9router/blob/master/open-sse/utils/error.js)
Error Configuration and Type Mapping
The open-sse/config/errorConfig.js file defines two critical lookup tables used by the error utilities:
ERROR_TYPES– Maps HTTP status codes to OpenAI-styletypeandcodevalues (e.g., 429 maps torate_limit_errorandrate_limit_exceeded).DEFAULT_ERROR_MESSAGES– Supplies friendly default messages when custom strings aren't provided, ensuring users always receive actionable feedback.ERROR_RULES– Flags specific error conditions withbackoff: trueto trigger exponential backoff calculations defined inBACKOFF_CONFIG.
Source: [open-sse/config/errorConfig.js](https://github.com/decolua/9router/blob/master/open-sse/config/errorConfig.js)
Standard Error Handling Flow in API Routes
The src/sse/handlers/chat.js file demonstrates the canonical five-step error handling pattern used across 9router routes.
1. Request Validation
Handlers immediately validate JSON payloads and required fields, returning errorResponse for malformed requests:
import { errorResponse } from "open-sse/utils/error.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
try {
body = await request.json();
} catch {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
}
if (!body.model) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}
Source: [src/sse/handlers/chat.js](https://github.com/decolua/9router/blob/master/src/sse/handlers/chat.js) (lines 30-35, 82-85)
2. Authentication Checks
Routes extract API keys via extractApiKey and validate them through isValidApiKey, returning HTTP_STATUS.UNAUTHORIZED for missing or invalid credentials:
if (!apiKey) {
return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Missing API key");
}
if (!(await isValidApiKey(apiKey))) {
return errorResponse(HTTP_STATUS.UNAUTHORIZED, "Invalid API key");
}
Source: [src/sse/handlers/chat.js](https://github.com/decolua/9router/blob/master/src/sse/handlers/chat.js) (lines 68-79)
3. Provider Credential Selection
When retrieving credentials via getProviderCredentials, handlers check if all accounts are rate-limited. If so, they invoke unavailableResponse to communicate backoff timing to clients:
if (!credentials || credentials.allRateLimited) {
return unavailableResponse(
status,
`[${provider}/${model}] ${errorMsg}`,
credentials.retryAfter,
credentials.retryAfterHuman
);
}
Source: [src/sse/handlers/chat.js](https://github.com/decolua/9router/blob/master/src/sse/handlers/chat.js) (lines 70-76)
4. Upstream Error Normalization
When forwarding requests to LLM providers, handlers use parseUpstreamError to translate provider-specific error formats into the standard OpenAI schema:
const providerResponse = await fetch(providerUrl, providerOpts);
if (!providerResponse.ok) {
const { statusCode, message } = await parseUpstreamError(providerResponse, executor);
return createErrorResult(statusCode, message);
}
Source: [open-sse/handlers/chatCore.js](https://github.com/decolua/9router/blob/master/open-sse/handlers/chatCore.js) (around line 217)
Reusable Patterns Across Route Handlers
Every API route in 9router imports the same error utilities, ensuring consistency across endpoints:
| Route | Validation Checks | Upstream Error Handling |
|---|---|---|
chat.js |
JSON body, model, API key, credentials | parseUpstreamError → createErrorResult |
tts.js |
JSON body, API key, model, input | parseUpstreamError → errorResponse |
stt.js |
Multipart form, API key, model, file | Same pattern as tts.js |
imageGeneration.js |
JSON body, API key, model, prompt | Same pattern |
fetch.js |
URL validation, provider support | Same pattern |
embeddings.js |
JSON body, model, input | Same pattern |
All handlers import:
import { errorResponse, unavailableResponse } from "open-sse/utils/error.js";
Source examples: [src/sse/handlers/tts.js](https://github.com/decolua/9router/blob/master/src/sse/handlers/tts.js), [src/sse/handlers/imageGeneration.js](https://github.com/decolua/9router/blob/master/src/sse/handlers/imageGeneration.js)
Rate Limiting and Retry Logic
When upstream providers return rate-limit errors, the credential manager (implemented in src/mitm/handlers/base.js) consults ERROR_RULES from errorConfig.js to calculate exponential backoff based on BACKOFF_CONFIG. The resulting retryAfter timestamp is passed to unavailableResponse, which sets the HTTP Retry-After header to inform clients when to retry.
Logging and Error Diagnostics
Every error path logs concise, masked messages via src/utils/logger.js. The formatProviderError helper enriches these logs with low-level network causes (e.g., ECONNRESET, ETIMEDOUT) while ensuring API keys and tokens remain masked in log output.
Source: [open-sse/utils/error.js](https://github.com/decolua/9router/blob/master/open-sse/utils/error.js) (see formatProviderError around line 39)
Practical Code Examples
Returning a Validation Error
Handle missing required fields with standard OpenAI-compatible error responses:
import { errorResponse } from "open-sse/utils/error.js";
import { HTTP_STATUS } from "open-sse/config/runtimeConfig.js";
export async function handleChat(request) {
const body = await request.json();
if (!body.model) {
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Missing model");
}
// ... processing continues
}
Result:
{
"error": {
"message": "Missing model",
"type": "invalid_request_error",
"code": "bad_request"
}
}
Normalizing Upstream Provider Errors
Wrap provider-specific failures into the standard format:
import { parseUpstreamError, createErrorResult } from "open-sse/utils/error.js";
const providerResponse = await fetch(providerUrl, options);
if (!providerResponse.ok) {
const { statusCode, message } = await parseUpstreamError(providerResponse);
return createErrorResult(statusCode, message);
}
Result (when provider returns 429):
{
"error": {
"message": "Rate limit exceeded",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}
Handling Complete Service Unavailability
When all provider accounts are throttled, return a 503 with retry guidance:
import { unavailableResponse } from "open-sse/utils/error.js";
if (credentials.allRateLimited) {
return unavailableResponse(
HTTP_STATUS.SERVICE_UNAVAILABLE,
"[anthropic/claude] All accounts rate-limited",
credentials.retryAfter,
credentials.retryAfterHuman
);
}
HTTP Response Headers:
Retry-After: 30
Response Body:
{
"error": {
"message": "[anthropic/claude] All accounts rate-limited (reset after 30s)"
}
}
Summary
- Centralize error handling using
open-sse/utils/error.jsto ensure all routes return OpenAI-compatible JSON schemas. - Validate early in route handlers using
errorResponsefor 400/401 errors before reaching upstream logic. - Normalize provider errors with
parseUpstreamErrorto abstract differences between OpenAI, Claude, and Gemini error formats. - Signal rate limits via
unavailableResponseto automatically communicateRetry-Afterheaders to clients. - Log safely using
formatProviderErrorto capture low-level network errors without exposing credentials.
Frequently Asked Questions
What is the standard error response format in 9router?
9router returns OpenAI-compatible JSON error objects containing three fields: message (human-readable description), type (error category like invalid_request_error), and code (machine-readable identifier like rate_limit_exceeded). This format is generated by buildErrorBody in open-sse/utils/error.js and consumed by client SDKs expecting OpenAI API compatibility.
How does 9router handle rate limit errors from upstream providers?
When an upstream provider returns a 429 status or when all configured credentials are exhausted, 9router invokes unavailableResponse with a calculated retryAfter value. This sets the HTTP Retry-After header and returns a 503 status code, allowing clients to implement intelligent backoff strategies automatically.
Where are error utilities centralized in the 9router codebase?
All error handling utilities reside in open-sse/utils/error.js, with configuration constants defined in open-sse/config/errorConfig.js. These modules provide errorResponse, unavailableResponse, parseUpstreamError, and createErrorResult, which are imported by every route handler including src/sse/handlers/chat.js, tts.js, and imageGeneration.js.
How can I add custom error handling to a new route in 9router?
Import the error utilities from open-sse/utils/error.js, then wrap validation logic using errorResponse for client errors (400/401) and parseUpstreamError combined with createErrorResult for upstream failures. For availability issues, use unavailableResponse if you need to signal retry timing to clients. Always validate request bodies before authentication to fail fast on malformed requests.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →