Errors
Every failure is a S3ndError with a stable code.
Everything s3nd throws is a S3ndError carrying a code you can branch on, and the
underlying error in cause:
import { isS3ndError } from 's3nd'
try {
await store.putSnapshot(code, state)
} catch (error) {
if (isS3ndError(error)) {
console.error(error.code, error.message, error.cause)
}
throw error
}Codes
| Code | When |
|---|---|
INVALID_CONFIG | Missing bucket, publicUrl that is not an absolute URL, non-positive maxSize. |
INVALID_SYNC_CODE | The code is empty, or holds characters outside the alphabet. |
INVALID_SNAPSHOT | Data JSON cannot represent, a stored object that is not a snapshot, an unreadable one, or an envelope from a newer format. |
SNAPSHOT_TOO_NEW | The snapshot's schema version is above the maxVersion you passed. |
PRECONDITION_FAILED | An ifMatch or ifAbsent write lost the race. |
INVALID_KEY | Empty key, leading or trailing /, .. segment, backslash, control character, over 1024 bytes. |
INVALID_BODY | The upload body is not a supported type. |
MISSING_CONTENT_LENGTH | A stream was uploaded without contentLength. |
FILE_TOO_LARGE | The body is above the configured maxSize. |
UPLOAD_FAILED | PutObject was rejected. Original error in cause. |
GET_FAILED | GetObject failed for a reason other than a missing key, or answered without a body. |
DELETE_FAILED | DeleteObject/DeleteObjects was rejected, or S3 reported per-key errors. |
URL_FAILED | Invalid expiresIn, signing failure, or a public URL requested with no publicUrl configured. |
Failures that never reach the network
INVALID_CONFIG, INVALID_SYNC_CODE, INVALID_KEY, INVALID_BODY, MISSING_CONTENT_LENGTH,
FILE_TOO_LARGE, and an INVALID_SNAPSHOT caused by unserializable data are all raised before a
single byte leaves your server. A user pasting nonsense into the code field, or a 200 MB database
posted to an endpoint capped at 4 MB, costs you a comparison rather than a request.
A missing snapshot is not an error
getSnapshot() returns null for a code that was never used and for one that has expired. Both
mean the same thing to the person typing it, so there is nothing to tell apart:
const snapshot = await store.getSnapshot(code)
if (!snapshot) {
return Response.json({ error: 'Unknown or expired code' }, { status: 404 })
}delete() follows S3's own semantics: deleting something that is not there succeeds quietly,
which is what makes a retry after a partial failure converge.
The three that are decisions, not bugs
Most codes mean something is wrong. These three are the system telling you something true, and what you do with them is a product choice:
PRECONDITION_FAILED — another device wrote first. Retry the read-merge-write loop, ask the
user, or refuse. See two devices, one snapshot.
SNAPSHOT_TOO_NEW — the snapshot came from a build ahead of this one. The only safe answer is
to stop and tell the user to update the app; importing anyway silently drops the fields this build
does not know about.
INVALID_SYNC_CODE — the typed code cannot exist. Better feedback than a lookup that finds
nothing, and it costs no request.
Mapping to HTTP
import { isS3ndError } from 's3nd'
const STATUS: Record<string, number> = {
FILE_TOO_LARGE: 413,
INVALID_BODY: 400,
INVALID_KEY: 400,
INVALID_SNAPSHOT: 400,
INVALID_SYNC_CODE: 400,
MISSING_CONTENT_LENGTH: 411,
PRECONDITION_FAILED: 409,
SNAPSHOT_TOO_NEW: 409,
}
export function statusFor(error: unknown): number {
return (isS3ndError(error) && STATUS[error.code]) || 500
}Keep UPLOAD_FAILED, GET_FAILED, DELETE_FAILED, URL_FAILED and INVALID_CONFIG on 500:
they mean something on your side is wrong, and they belong in your logs with cause attached.