Error Handling
Error response format, common error codes (400, 401, 403, 404, 429, 500), rate limit errors, and retry strategies.
NFYio APIs return structured error responses with consistent format and HTTP status codes. Use this guide to handle errors gracefully and implement retry logic.
Error Response Format
All API errors follow this structure:
{
"error": {
"code": "InvalidRequest",
"message": "Human-readable description of what went wrong",
"details": {
"field": "bucket_name",
"reason": "Bucket name must be 3-63 characters"
}
}
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable error description |
details | object | Optional additional context (varies) |
Common Error Codes
400 Bad Request
Invalid request syntax or parameters.
| Code | Description |
|---|---|
InvalidRequest | Malformed JSON, missing required field |
InvalidParameter | Parameter value invalid or out of range |
ValidationError | Request failed validation (e.g., Zod schema) |
Example:
{
"error": {
"code": "InvalidParameter",
"message": "Bucket name must be 3-63 characters and contain only lowercase letters, numbers, and hyphens",
"details": { "field": "name", "value": "My Bucket!" }
}
}
401 Unauthorized
Missing or invalid authentication.
| Code | Description |
|---|---|
Unauthorized | No credentials provided |
InvalidToken | JWT expired or malformed |
InvalidApiKey | API key invalid or revoked |
Example:
{
"error": {
"code": "InvalidToken",
"message": "JWT has expired"
}
}
Fix: Refresh the token or use a valid API key.
403 Forbidden
Authenticated but not authorized for the requested action.
| Code | Description |
|---|---|
Forbidden | Insufficient permissions |
ScopeRequired | API key missing required scope |
ResourceLocked | Resource is locked by another process |
Example:
{
"error": {
"code": "ScopeRequired",
"message": "API key does not have write:objects scope",
"details": { "required_scope": "write:objects" }
}
}
404 Not Found
Resource does not exist.
| Code | Description |
|---|---|
NotFound | Resource not found |
BucketNotFound | Bucket does not exist |
ObjectNotFound | Object does not exist |
Example:
{
"error": {
"code": "BucketNotFound",
"message": "Bucket 'my-bucket' does not exist"
}
}
429 Too Many Requests
Rate limit exceeded. See Rate Limits for plan-specific limits.
| Code | Description |
|---|---|
RateLimitExceeded | Too many requests in time window |
QuotaExceeded | Plan quota exceeded |
Example:
{
"error": {
"code": "RateLimitExceeded",
"message": "Rate limit exceeded. Retry after 60 seconds.",
"details": {
"retry_after": 60,
"limit": 1000,
"window": "1m"
}
}
}
Response headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1709304600
Retry-After: 60
500 Internal Server Error
Server-side error. Retry with exponential backoff.
| Code | Description |
|---|---|
InternalError | Unexpected server error |
ServiceUnavailable | Temporary outage, retry later |
Example:
{
"error": {
"code": "InternalError",
"message": "An unexpected error occurred. Please try again."
}
}
Retry Strategies
Exponential Backoff
For 429 and 5xx errors, retry with increasing delay:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.ok) return response;
const isRetryable = response.status === 429 || response.status >= 500;
if (!isRetryable || attempt === maxRetries) {
throw new Error(`Request failed: ${response.status}`);
}
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.pow(2, attempt) * 1000;
await new Promise((r) => setTimeout(r, delay));
}
}
Respect Retry-After
When the server sends Retry-After, wait at least that long before retrying:
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return fetchWithRetry(url, options); // Retry
}
Idempotency
For write operations (PUT, POST, DELETE), ensure your retries are idempotent. Use idempotency keys when supported:
curl -X POST https://api.yourdomain.com/v1/buckets \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: unique-key-123" \
-H "Content-Type: application/json" \
-d '{"name": "my-bucket"}'
Storage-Specific Errors
S3-compatible storage may return XML errors:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchBucket</Code>
<Message>The specified bucket does not exist</Message>
<BucketName>my-bucket</BucketName>
</Error>
Map these to the same concepts: NoSuchBucket → 404, AccessDenied → 403, etc.
Best Practices
- Parse error body — Check
error.codefor programmatic handling - Log details — Include
error.messageanderror.detailsin logs - Retry 429 and 5xx — Use exponential backoff and
Retry-After - Don’t retry 4xx — Client errors (except 429) usually won’t succeed on retry
- User-facing messages — Use
error.messagefor display; avoid exposing internal details
Next Steps
- Rate Limits — Plan-specific limits and headers
- API Authentication — Fixing 401/403 errors
- SDKs & Libraries — SDKs may include built-in retry logic