Error Handling
Seed implements consistent error handling across all endpoints using standardized error formats based on JSON-RPC 2.0 for MCP endpoints and OAuth 2.0 RFC standards for OAuth endpoints.
Error Response Formats
JSON-RPC 2.0 Format
Used for MCP endpoints and general server errors.
Structure:
{
"jsonrpc": "2.0",
"error": {
"code": <number>,
"message": "<string>",
"data": {
"reason": "<string>",
"details": "<string>",
...
}
},
"id": <number|string|null>
}Fields:
jsonrpc(string): Always"2.0"error(object): Error detailscode(number): JSON-RPC error codemessage(string): Human-readable error messagedata(object, optional): Additional error contextreason(string): Machine-readable error reasondetails(string): Additional human-readable details
id(number|string|null): Request ID (null for errors without corresponding request)
Example:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "invalid_token",
"details": "JWT signature verification failed"
}
},
"id": null
}OAuth 2.0 Error Format
Used for OAuth endpoints per RFC 6749.
Structure:
{
"error": "<string>",
"error_description": "<string>"
}Fields:
error(string): OAuth error codeerror_description(string, optional): Human-readable error description
Example:
{
"error": "invalid_client",
"error_description": "Unknown or expired client_id"
}DCR Error Format
Used for Dynamic Client Registration per RFC 7591.
Structure:
{
"error": "<string>",
"error_description": "<string>"
}DCR-Specific Error Codes:
invalid_redirect_uri: Redirect URI validation failedinvalid_client_metadata: Client metadata validation failedinvalid_software_statement: Software statement invalid (not used)server_error: Server-side processing error
Example:
{
"error": "invalid_redirect_uri",
"error_description": "redirect_uri must use https"
}HTTP Status Codes
Seed uses standard HTTP status codes with consistent meanings:
| Status | Name | Usage |
|---|---|---|
| 200 | OK | Successful request |
| 201 | Created | Resource created (DCR) |
| 302 | Found | OAuth authorization redirect |
| 400 | Bad Request | Invalid request parameters |
| 401 | Unauthorized | Authentication required or failed |
| 403 | Forbidden | Valid auth but insufficient permissions |
| 404 | Not Found | Resource not found (session, endpoint) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side processing error |
| 502 | Bad Gateway | Upstream service error |
Authentication Errors
Missing Token
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "missing_token",
"details": "No Authorization header provided"
}
},
"id": null
}Headers:
WWW-Authenticate: Bearer resource_metadata="https://seed.example.com/.well-known/oauth-protected-resource"Trigger: Request to protected endpoint without Authorization header
Invalid Format
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "invalid_format",
"details": "Authorization header must use Bearer scheme"
}
},
"id": null
}Trigger: Authorization header present but not in format Bearer <token>
Invalid Token
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "invalid_token",
"details": "JWT signature verification failed"
}
},
"id": null
}Trigger: JWT signature verification fails (invalid signature, malformed token)
Expired Token
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "expired_token",
"details": "Token expired at 2026-01-05T12:00:00Z"
}
},
"id": null
}Trigger: JWT exp claim is in the past
Invalid Issuer
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "invalid_issuer",
"details": "Token issuer does not match expected issuer"
}
},
"id": null
}Trigger: JWT iss claim doesn't match OIDC_ISSUER configuration
Invalid Audience
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "invalid_audience",
"details": "Token audience does not match expected audience"
}
},
"id": null
}Trigger: JWT aud claim doesn't match OIDC_AUDIENCE configuration
Missing Claim
HTTP Status: 401 Unauthorized
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "Unauthorized",
"data": {
"reason": "missing_claim",
"details": "Required claim 'sub' not found in token"
}
},
"id": null
}Trigger: JWT missing required sub claim
MCP Session Errors
Invalid or Expired Session
HTTP Status: 404 Not Found
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Invalid or expired session",
"data": {
"reason": "session_not_found"
}
},
"id": null
}Trigger: mcp-session-id header references non-existent or expired session
Cause:
- Session never existed
- Session expired (24-hour TTL exceeded)
- Session was explicitly terminated via
DELETE /mcp
Missing Session ID
HTTP Status: 400 Bad Request
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Bad Request",
"data": {
"reason": "missing_session_id",
"details": "mcp-session-id header required for non-initialize requests"
}
},
"id": null
}Trigger: Request to /mcp without mcp-session-id header (except initialize method)
Rate Limiting Errors
Rate Limit Exceeded
HTTP Status: 429 Too Many Requests
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Too Many Requests",
"data": {
"reason": "rate_limit_exceeded",
"details": "Rate limit exceeded. Please try again later.",
"retryAfter": 45
}
},
"id": null
}Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704643200
Retry-After: 45Trigger: Per-IP rate limit exceeded
Rate Limits:
- MCP endpoints: 100 requests/minute per IP
- DCR endpoint: 10 registrations/hour per IP
Global Rate Limit Exceeded
HTTP Status: 429 Too Many Requests
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Too Many Requests",
"data": {
"reason": "global_rate_limit_exceeded",
"details": "Global rate limit exceeded. Please try again later.",
"retryAfter": 45
}
},
"id": null
}Trigger: Global rate limit exceeded (all IPs combined)
Global Limits:
- MCP endpoints: 10,000 requests/minute globally
- DCR endpoint: 1,000 registrations/hour globally
OAuth Errors
Authorization Errors
Invalid Client (authorization endpoint):
HTTP Status: 400 Bad Request
Response:
{
"error": "invalid_client",
"error_description": "Unknown or expired client_id"
}Trigger: DCR client not found in Redis or expired
Invalid Redirect URI (authorization endpoint):
HTTP Status: 400 Bad Request
Response:
{
"error": "invalid_redirect_uri",
"error_description": "redirect_uri does not match any registered URI"
}Trigger: redirect_uri not in client's registered URIs
Server Error (authorization endpoint):
HTTP Status: 500 Internal Server Error
Response:
{
"error": "server_error",
"error_description": "Authorization endpoint not configured"
}Trigger: OAUTH_AUTHORIZATION_URL not configured
Token Errors
Invalid Request (token endpoint):
HTTP Status: 400 Bad Request
Response:
{
"error": "invalid_request",
"error_description": "Missing required parameter: code"
}Trigger: Required parameter missing from token request
Possible Variations:
"Missing required parameter: code""Missing required parameter: redirect_uri""Missing required parameter: client_id""Missing required parameter: code_verifier (PKCE required)""Missing required parameter: refresh_token""Missing required parameter: grant_type"
Unsupported Grant Type (token endpoint):
HTTP Status: 400 Bad Request
Response:
{
"error": "unsupported_grant_type",
"error_description": "Unsupported grant type: implicit"
}Trigger: grant_type not in ["authorization_code", "refresh_token"]
Invalid Grant (token endpoint):
HTTP Status: 400 Bad Request
Response:
{
"error": "invalid_grant",
"error_description": "redirect_uri does not match registered URI"
}Trigger: redirect_uri in token request doesn't match authorization request
Invalid Client (token endpoint):
HTTP Status: 401 Unauthorized
Response:
{
"error": "invalid_client",
"error_description": "Unknown or expired client_id"
}Trigger: DCR client not found in Redis or expired (during token exchange)
Server Error (token endpoint):
HTTP Status: 500 Internal Server Error
Response:
{
"error": "server_error",
"error_description": "Token endpoint not configured"
}Trigger: OAUTH_TOKEN_URL not configured
Bad Gateway (token endpoint):
HTTP Status: 502 Bad Gateway
Response:
{
"error": "server_error",
"error_description": "Failed to contact authorization server: <error>"
}Trigger: Network error contacting upstream IdP
DCR Errors
Invalid Redirect URI (DCR):
HTTP Status: 400 Bad Request
Response:
{
"error": "invalid_redirect_uri",
"error_description": "redirect_uri must use https"
}Trigger: Redirect URI validation failure
Possible Variations:
"redirect_uri must use https (got http:)""redirect_uri must not contain a fragment""redirect_uri is not a valid URL"
Invalid Client Metadata (DCR):
HTTP Status: 400 Bad Request
Response:
{
"error": "invalid_client_metadata",
"error_description": "redirect_uris exceeds maximum of 10"
}Trigger: Client metadata validation failure
Possible Variations:
"Missing required parameter: redirect_uris""redirect_uris must be an array""redirect_uris must contain at least one URI""redirect_uris exceeds maximum of 10""redirect_uris must contain only strings""grant_types must be an array""Invalid grant_type: implicit. Supported: authorization_code, refresh_token""response_types must be an array""Invalid response_type: token. Supported: code""Invalid token_endpoint_auth_method: client_secret_jwt. Supported: none, client_secret_post, client_secret_basic""client_name must be a string""software_id must be a string""software_version must be a string"
Server Error (DCR):
HTTP Status: 500 Internal Server Error
Response:
{
"error": "server_error",
"error_description": "Failed to register client"
}Trigger: Redis error or other server-side processing failure during registration
Security Errors
Origin Validation Failed
HTTP Status: 403 Forbidden
Response:
{
"jsonrpc": "2.0",
"error": {
"code": -32000,
"message": "Forbidden",
"data": {
"reason": "invalid_origin",
"details": "Origin not allowed by server policy"
}
},
"id": null
}Trigger: Origin header present but not in allowed origins list
Cause: DNS rebinding attack prevention
See Also: Security
Error Code Reference
JSON-RPC Error Codes
| Code | Category | Usage |
|---|---|---|
-32001 | Authentication | Auth failures (401) |
-32000 | Server | General server errors (400, 403, 404, 429, 500) |
Note: JSON-RPC standard reserves codes -32768 to -32000 for predefined errors. Seed uses custom codes in this range.
OAuth Error Codes
Per RFC 6749 Section 5.2:
| Error Code | HTTP Status | Description |
|---|---|---|
invalid_request | 400 | Missing or invalid request parameter |
invalid_client | 401 | Client authentication failed |
invalid_grant | 400 | Authorization grant invalid/expired |
unauthorized_client | 400 | Client not authorized for grant type |
unsupported_grant_type | 400 | Grant type not supported |
invalid_scope | 400 | Requested scope invalid/unknown |
server_error | 500 | Server encountered error |
DCR Error Codes
Per RFC 7591:
| Error Code | HTTP Status | Description |
|---|---|---|
invalid_redirect_uri | 400 | Redirect URI validation failed |
invalid_client_metadata | 400 | Client metadata validation failed |
invalid_software_statement | 400 | Software statement invalid |
server_error | 500 | Server encountered error |
Error Context
Additional context provided in error.data for debugging:
Authentication Context
Fields:
reason(string): Machine-readable error codedetails(string): Human-readable explanation
Example:
{
"reason": "expired_token",
"details": "Token expired at 2026-01-05T12:00:00Z"
}Rate Limiting Context
Fields:
reason(string):rate_limit_exceededorglobal_rate_limit_exceededdetails(string): Human-readable messageretryAfter(number): Seconds until retry allowed
Example:
{
"reason": "rate_limit_exceeded",
"details": "Rate limit exceeded. Please try again later.",
"retryAfter": 45
}Session Context
Fields:
reason(string):session_not_foundormissing_session_id
Example:
{
"reason": "session_not_found"
}Origin Validation Context
Fields:
reason(string):invalid_origindetails(string): Policy violation message
Example:
{
"reason": "invalid_origin",
"details": "Origin not allowed by server policy"
}Client Error Handling Recommendations
Retry Strategy
Authentication Errors (401):
- ❌ Do NOT retry with same token
- ✅ Refresh token if available
- ✅ Re-authenticate if no refresh token
- ❌ Do NOT retry more than once
Rate Limit Errors (429):
- ✅ Respect
Retry-Afterheader - ✅ Implement exponential backoff if no header
- ✅ Use
X-RateLimit-Resetfor next window - ❌ Do NOT retry immediately
Session Errors (404):
- ✅ Re-initialize session with
initializerequest - ❌ Do NOT retry with same session ID
Server Errors (500, 502):
- ✅ Retry with exponential backoff
- ✅ Maximum 3 retries
- ✅ Log for debugging
Client Errors (400, 403):
- ❌ Do NOT retry
- ✅ Fix request parameters
- ✅ Log for debugging
Error Parsing
JSON-RPC Errors:
interface JsonRpcError {
jsonrpc: "2.0";
error: {
code: number;
message: string;
data?: {
reason?: string;
details?: string;
retryAfter?: number;
};
};
id: number | string | null;
}
function isJsonRpcError(response: unknown): response is JsonRpcError {
return (
typeof response === "object" &&
response !== null &&
"error" in response &&
"jsonrpc" in response
);
}OAuth Errors:
interface OAuthError {
error: string;
error_description?: string;
}
function isOAuthError(response: unknown): response is OAuthError {
return (
typeof response === "object" &&
response !== null &&
"error" in response &&
typeof response.error === "string"
);
}Logging Best Practices
Client-Side Logging:
// Log full error for debugging
console.error("API error", {
status: response.status,
error: errorBody,
requestId: response.headers.get("x-request-id"),
timestamp: new Date().toISOString(),
});
// Show user-friendly message
if (errorBody.error.code === -32001) {
showError("Authentication failed. Please log in again.");
} else if (response.status === 429) {
const retryAfter = errorBody.error.data?.retryAfter ?? 60;
showError(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
}Implementation Details
Error Response Helper (MCP/Auth)
File: src/middleware/auth.ts
function sendAuthError(
res: Response,
reason: string,
details: string
): void {
res.status(401);
res.set("WWW-Authenticate", `Bearer resource_metadata="${config.baseUrl}/.well-known/oauth-protected-resource"`);
res.json({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Unauthorized",
data: {
reason,
details,
},
},
id: null,
});
}Error Response Helper (OAuth)
File: src/routes/oauth-token.ts
function sendOAuthError(
res: Response,
error: string,
description: string,
status = 400
): void {
res.status(status).json({
error,
error_description: description,
});
}Error Response Helper (DCR)
File: src/routes/oauth-register.ts
function sendRegistrationError(
res: Response,
error: RegistrationErrorCode,
description: string,
status = 400
): void {
res.status(status).json({
error,
error_description: description,
});
}Testing Error Responses
Example Test Cases
Authentication Error:
it("should return 401 for missing token", async () => {
const response = await request(app)
.post("/mcp")
.send({ jsonrpc: "2.0", method: "initialize", id: 1 });
expect(response.status).toBe(401);
expect(response.body.error.code).toBe(-32001);
expect(response.body.error.data.reason).toBe("missing_token");
});Rate Limit Error:
it("should return 429 after rate limit exceeded", async () => {
// Make 101 requests
for (let i = 0; i < 101; i++) {
await request(app).post("/mcp").set("Authorization", `Bearer ${token}`);
}
const response = await request(app)
.post("/mcp")
.set("Authorization", `Bearer ${token}`);
expect(response.status).toBe(429);
expect(response.body.error.data.reason).toBe("rate_limit_exceeded");
expect(response.headers["retry-after"]).toBeDefined();
});OAuth Error:
it("should return invalid_client for unknown client", async () => {
const response = await request(app)
.post("/oauth/token")
.send({
grant_type: "authorization_code",
code: "abc123",
client_id: "seed-nonexistent",
redirect_uri: "https://app.example.com/callback",
code_verifier: "verifier",
});
expect(response.status).toBe(401);
expect(response.body.error).toBe("invalid_client");
});Related Documentation
- Authentication Flow - Authentication error details
- Rate Limiting - Rate limit error behavior
- OAuth 2.1 Implementation - OAuth error standards
- API Endpoints - Endpoint-specific error responses
- Observability - Error logging and metrics