Errors
Four classes cover every failure path in sonuslab-storage.
Error classes
| Name | Surface | Description |
|---|---|---|
StorageApiError | server SDK | Non-2xx response from storage-api. Exposes .status (number) and .body (parsed JSON or string). |
UploadError | browser | Network or PUT failure in the browser helper. Exposes .cause with the underlying error. |
WebhookVerificationError | webhook | Thrown by verifyWebhook on missing secret, bad signature, expired timestamp, or malformed payload. |
DecryptionError | crypto | Thrown by decryptBytes / decryptContainer on a wrong key, an unsupported container version, a tampered header, or a corrupt payload. Exposes .cause. |
StorageApiError
Every server SDK call rejects with this class on a non-2xx response. Switch on .status to map to your own HTTP responses.
import { StorageApiError } from 'sonuslab-storage/server'
try {
await storage.upload({ name, contentType, data })
} catch (err) {
if (err instanceof StorageApiError) {
if (err.status === 401) // invalid or revoked API key
if (err.status === 413) // file exceeds maxFileSize
if (err.status === 404) // file id not found
if (err.status === 429) // rate limited (see below)
console.error(err.status, err.body)
}
throw err
}
Common statuses
| Code | Meaning | Description |
|---|---|---|
400 | Bad Request | Request body failed validation. .body has the field-level error. |
401 | Unauthorized | API key missing, invalid, or revoked. |
404 | Not Found | File or upload id does not exist (or belongs to a different app). |
413 | Payload Too Large | size exceeds the app maxFileSize. Use multipart instead. |
429 | Too Many Requests | Rate limit hit. Honour the Retry-After header. |
5xx | Server Error | Transient storage-api error. Retry with exponential backoff. |
WebhookVerificationError
Thrown by verifyWebhook when:
- The signature header is missing or malformed.
- The HMAC doesn't match (wrong secret, modified body).
- The timestamp is older than the tolerance window (default 300s).
- The secret is empty or missing — it is never treated as a valid key.
Always respond with 401 — do not leak which check failed.
import { WebhookVerificationError } from 'sonuslab-storage/webhook'
try {
verifyWebhook({ body, signature, secret })
} catch (err) {
if (err instanceof WebhookVerificationError) {
throw createError({ statusCode: 401, statusMessage: 'Invalid signature' })
}
throw err
}
UploadError
Wraps any failure during the browser upload flow — presign request, the direct PUT, abort, encryption, or finalize. The original error is preserved in .cause.
message is a fixed, safe summary — upstream R2/S3 bodies name the object key and carry request and host ids, and adapters surface message straight into toasts and error trackers, so the details live on .cause instead. Inspect .cause (it carries status and body for HTTP failures); code that string-matches on message will break.import { UploadError } from 'sonuslab-storage/client'
try {
await uploadFile({ file, presignEndpoint, completeEndpoint })
} catch (err) {
if (err instanceof UploadError) {
console.error('upload failed:', err.message, err.cause)
toast.error('Could not upload — please try again')
return
}
throw err
}
Patterns
Map server errors to HTTP responses
export default defineEventHandler(async (event) => {
try {
return await storage.presign(await readBody(event))
} catch (err) {
if (err instanceof StorageApiError) {
throw createError({ statusCode: err.status, statusMessage: err.body?.message ?? 'Upload error' })
}
throw err
}
})
Retry on 5xx / 429
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; i < attempts; i++) {
try {
return await fn()
} catch (err) {
const retryable = err instanceof StorageApiError && (err.status === 429 || err.status >= 500)
if (!retryable || i === attempts - 1) throw err
await new Promise((r) => setTimeout(r, 250 * 2 ** i))
}
}
throw new Error('unreachable')
}
const file = await withRetry(() => storage.upload({ name, contentType, data }))
