Errors
The error catalog, building errors with s3Error, and handling APIError.
Every failed request returns an HTTP status and a JSON body:
{
"code": "FILE_TOO_LARGE",
"message": "File is 5242880 bytes; the limit is 2097152 bytes",
"params": { "size": 5242880, "maxFileSize": 2097152 }
}code is always set and stable. message is English, filled from params.
Show users your own text: map code and params to it, as
useFormatDimahError does.
Throwing errors
Throw s3Error with a catalog code to reject a call from a guard or hook.
Codes with params require them:
import { s3Error } from "@dimah-s3/server";
throw new Error("Not allowed");
throw s3Error("UNAUTHORIZED");
throw s3Error("FORBIDDEN");
throw s3Error("OBJECT_NOT_FOUND");
throw s3Error("QUOTA_EXCEEDED", { limit: "bytes" });
throw s3Error("FILE_TOO_LARGE", { size: file.size, maxFileSize });A plain Error, thrown anywhere, is logged, passed to onError, and returns
500 INTERNAL_ERROR. Its message never reaches the client.
For a code of your own, such as from a plugin, use APIError.from:
import { APIError } from "@dimah-s3/server";
throw APIError.from(402, {
code: "PLAN_REQUIRED",
message: "Upgrade your plan to upload videos",
});Handling errors
createS3Client and the React hooks reject with an APIError. Narrow it by
code with isS3ErrorCode:
import { isAPIError, isS3ErrorCode } from "@dimah-s3/core";
try {
await api.download({ route: "avatar", key });
} catch (error) {
if (isS3ErrorCode(error, "OBJECT_NOT_FOUND")) {
// Handle the missing object
} else if (isAPIError(error)) {
console.log(error.statusCode, error.code, error.message, error.params);
}
}The client turns every failure into an APIError:
- A JSON error body keeps its
code,params, and HTTP status. Aparamsentry that is not a string or number is dropped; the rest of the error is kept. - Any other error response becomes
INTERNAL_ERROR(status 500 or above) orUNKNOWN_ERROR, with the real HTTP status. - A
2xxresponse whose body is not a JSON object or array, such as an SPA fallbackindex.html, becomesUNKNOWN_ERRORwith its status. - The browser upload transport raises
CHECKSUM_MISMATCHwhen S3 rejects a checksummed PUT or part (BadDigestorXAmzContentChecksumMismatch). - A request that fails before a response becomes
NETWORK_ERROR. - An aborted request rejects with the signal's abort reason, not an
APIError.
deleteMany reports per-key failures as wire bodies in failed[].error.
Rebuild one with APIError.fromShape(error); the status comes from the
catalog.
Reporting errors
onError receives every failure that is not an APIError: plain errors,
config errors, and S3 network errors. They are logged too, then answered with
500 INTERNAL_ERROR or 502 S3_NETWORK_ERROR. Rejections you throw with
s3Error do not reach it. The context has the request and, when known,
the operation. It may be async: a rejection from it is logged, never
unhandled. An error from another library that is merely named APIError is
an unexpected failure too, so it is logged and reaches onError.
export const s3 = dimahS3({
onError: async (error, { request, operation }) => {
Sentry.captureException(error, { extra: { url: request.url, operation } });
},
// ...
});A throw from your upload.onConfirmed, even an APIError, returns
500 INTERNAL_ERROR and keeps the object, so the client can retry the
confirm. onError gets a wrapping Error with the original as cause.
Reject content in confirmGuard instead; a rejection there deletes the
object.
Server codes
Codes the server sends. The status is in S3_ERROR_CODES[code].status.
code | HTTP | params | When |
|---|---|---|---|
VALIDATION_ERROR | 400 | issue | The body or query does not match the endpoint schema. |
REQUEST_TOO_LARGE | 413 | maxSize | The request body is larger than maxRequestBodySize. |
UNSUPPORTED_MEDIA_TYPE | 415 | — | A body endpoint got a body that is not JSON. |
NOT_FOUND | 404 | — | No endpoint serves the request path. |
UNAUTHORIZED | 401 | — | A guard rejects an anonymous caller. |
FORBIDDEN | 403 | — | A guard or authorize rejects the caller. |
CONFLICT | 409 | — | A concurrent change won, or db() purge is cleaning the key. A retry may succeed. |
INTERNAL_ERROR | 500 | — | Unhandled server error, or a non-JSON error response with status 500 or above. |
S3_NETWORK_ERROR | 502 | code | The server cannot reach the S3 endpoint. |
UNKNOWN_ROUTE | 404 | route | The route name is not registered on the server. |
FEATURE_DISABLED | 404 | feature | The route does not enable the requested operation, or /file was asked for a route that is not in proxy mode. |
KEY_INVALID | 400 | — | The key is unsafe, or outside the route keyPrefix. |
OBJECT_NOT_FOUND | 404 | — | The object or multipart upload does not exist, or db() tracks it for another scope. |
RANGE_NOT_SATISFIABLE | 416 | — | A /file Range header is outside the object. |
PRECONDITION_FAILED | 412 | — | A /file If-Match or If-Unmodified-Since fails. |
FILE_EMPTY | 400 | — | The file has 0 bytes. validateFile checks it before upload. |
FILE_TOO_LARGE | 413 | size, maxFileSize | The file is larger than the route maxFileSize. |
FILE_TYPE_NOT_ALLOWED | 400 | type, allowed | The file type is not in the route fileTypes. |
FILE_CONTENT_MISMATCH | 400 | type, detected | With verifyContent, the stored bytes do not match the declared type. |
CHECKSUM_REQUIRED | 400 | — | The route needs a SHA-256 checksum and none was sent. |
QUOTA_EXCEEDED | 403 | limit | The upload would go over the db() byte or file quota. |
UPLOAD_TICKET_INVALID | 403 | — | The upload ticket is forged, for another call, or for another subject. |
UPLOAD_TICKET_EXPIRED | 410 | — | The upload ticket is past its expiry. |
UPLOAD_SIZE_MISMATCH | 400 | expected, actual | The stored size differs from the declared size. |
MULTIPART_PART_MISSING | 400 | partNumber | Complete found a part that was never uploaded. |
MULTIPART_PART_INVALID | 400 | partNumber, partCount | The part number is outside the server's layout. |
OBJECT_EXISTS | 409 | — | A db() route without replace: "overwrite" already holds an object at the key. |
FOLDER_INVALID | 400 | — | The db() folder path is not valid, or deeper than 32 levels. |
FOLDER_EXISTS | 409 | — | A db() folder with that path already exists. |
FOLDER_NOT_EMPTY | 409 | — | A db() folder still holds folders or objects. |
Client codes
Codes only the browser raises (client: true in the catalog). They never
come from the server.
code | HTTP | params | When |
|---|---|---|---|
TOO_MANY_FILES | 400 | maxFiles | More files were added than maxFiles allows. |
HOOK_FAILED | 500 | hook | One of your callbacks, such as onSuccess, threw. |
NETWORK_ERROR | 502 | — | The browser request failed before a response arrived. |
UPLOAD_FAILED | 502 | status | S3 rejected the upload. status is S3's HTTP status. |
DOWNLOAD_FAILED | 502 | status | useFetchDownload got a non-2xx response. |
CHECKSUM_MISMATCH | 400 | — | The uploaded bytes do not match their checksum. |
PARTIAL_FAILURE | 207 | failed, total | Some, but not all, files or keys in one call failed. |
UNKNOWN_ERROR | 500 | — | Any other failure, or a non-JSON error response below status 500. |
Frequently asked questions
Use useFormatDimahError from @dimah-s3/react. It maps each code and its
params to a translated string:
import { useDownload, useFormatDimahError } from "@dimah-s3/react";
const formatError = useFormatDimahError();
const { download } = useDownload({
route: "avatar",
onError: (error) => toast.error(formatError(error)),
});S3_ERROR_CODES holds each entry's status, message template, and
client flag. formatErrorMessage(template, params) fills a template:
import { formatErrorMessage, S3_ERROR_CODES } from "@dimah-s3/core";
formatErrorMessage(S3_ERROR_CODES.UPLOAD_FAILED.message, { status: 403 });
// "Upload failed with HTTP 403"