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) – Wraps buildErrorBody in an HTTP Response with appropriate Content-Type and CORS headers.
  • unavailableResponse(statusCode, message, retryAfter, retryAfterHuman) – Used when every credential for a provider is rate-limited; adds a Retry-After header 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 like ECONNRESET without 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-style type and code values (e.g., 429 maps to rate_limit_error and rate_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 with backoff: true to trigger exponential backoff calculations defined in BACKOFF_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 parseUpstreamErrorcreateErrorResult
tts.js JSON body, API key, model, input parseUpstreamErrorerrorResponse
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.js to ensure all routes return OpenAI-compatible JSON schemas.
  • Validate early in route handlers using errorResponse for 400/401 errors before reaching upstream logic.
  • Normalize provider errors with parseUpstreamError to abstract differences between OpenAI, Claude, and Gemini error formats.
  • Signal rate limits via unavailableResponse to automatically communicate Retry-After headers to clients.
  • Log safely using formatProviderError to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →