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"
    }
  }
}
FieldTypeDescription
codestringMachine-readable error code
messagestringHuman-readable error description
detailsobjectOptional additional context (varies)

Common Error Codes

400 Bad Request

Invalid request syntax or parameters.

CodeDescription
InvalidRequestMalformed JSON, missing required field
InvalidParameterParameter value invalid or out of range
ValidationErrorRequest 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.

CodeDescription
UnauthorizedNo credentials provided
InvalidTokenJWT expired or malformed
InvalidApiKeyAPI 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.

CodeDescription
ForbiddenInsufficient permissions
ScopeRequiredAPI key missing required scope
ResourceLockedResource 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.

CodeDescription
NotFoundResource not found
BucketNotFoundBucket does not exist
ObjectNotFoundObject 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.

CodeDescription
RateLimitExceededToo many requests in time window
QuotaExceededPlan 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.

CodeDescription
InternalErrorUnexpected server error
ServiceUnavailableTemporary 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

  1. Parse error body — Check error.code for programmatic handling
  2. Log details — Include error.message and error.details in logs
  3. Retry 429 and 5xx — Use exponential backoff and Retry-After
  4. Don’t retry 4xx — Client errors (except 429) usually won’t succeed on retry
  5. User-facing messages — Use error.message for display; avoid exposing internal details

Next Steps