Errors
Every error the framework raises carries four things: a code you can branch on, an HTTP status, a message describing what happened, and usually a hint describing how to fix it.
import server, { ServerError } from '@server/next';
server({
onError: (error, ctx) => {
error.code; // 'UPLOAD_TOO_LARGE'
error.status; // 413
error.message; // 'File "avatar.png" is too large (4200000 bytes, limit is 10mb)'
error.hint; // "Raise it with `uploads: { bucket, maxFileSize: '50mb' }` ..."
return new Response(error.message, { status: error.status });
},
});What reaches the client
The message and the hint have different audiences, so they go to different places.
A 4xx describes what the client got wrong, so its message is sent to them. A 5xx describes what went wrong inside the app, so the client only gets Server Error and the real message, its hint and its stack go to your log. That holds for errors your own handlers throw too: a throw new Error(...) has no status, so it is a 500, so its message never leaves the server.
| client receives | log receives | |
|---|---|---|
4xx | the message | nothing, it isn't your bug |
5xx | Server Error | message, hint, docs link, stack |
a handler's own throw | Server Error | the error, in full |
The hint is never sent in production, whatever the status: it names your options and your environment variables, which is information the client has no use for.
In development
When NODE_ENV is not production and the request came from a browser, an error renders as a page with the message, the hint and a link to this reference. Anything else (a fetch, curl, an SDK) still gets the plain body, so your API behaves the same in every environment.
The framework also prints one line at startup so a development build is never mistaken for a deployed one:
[server:app] Running in development mode. Set NODE_ENV=production when you deploy.Handling them yourself
onError receives the error and the context, and whatever it returns is sent. Branch on error.code rather than on the message, which is prose and may change:
server({
onError: (error, ctx) => {
if (error.code === 'UPLOAD_TOO_LARGE') {
return { error: 'That file is too big, keep it under 10MB' };
}
if (error.status >= 500) report(error); // your own logging
return new Response(error.message, { status: error.status || 500 });
},
});A validation failure carries the failing fields, so an API can answer with them:
if (error.code === 'INVALID_REQUEST') {
return status(422).json({ errors: error.issues });
}Throwing your own
ServerError takes a code, a status and a message, and behaves exactly like the built-in ones:
import { ServerError } from '@server/next';
throw new ServerError('TEAM_FULL', 409, 'This team already has 10 members');Register codes you use often and they become factories, with {placeholders} filled from the argument:
ServerError.extend({
TEAM_FULL: { status: 409, message: 'The team "{name}" already has {max} members' },
});
throw ServerError.TEAM_FULL({ name: 'Ops', max: '10' });Reference
The request
Raised before your handler runs.
NOT_FOUND
404 Not Found
No route matched. Register a catch-all last to answer with your own page, since routes are tried in the order they were added and the first match wins:
server()
.get('/', Home)
.get('/about', About)
.get(() => <MissingPage />); // no path: matches everything leftMETHOD_NOT_ALLOWED
405 The HTTP method "{method}" is not supported
Only GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS are routed. A client sending anything else is usually a proxy or a scanner.
PATH_TRAVERSAL
400 The route param '{param}' tries to climb the path ('{value}')
A route param pointed outside where it belongs. If this route legitimately receives paths, set security: { traversalProtection: false }.
INVALID_REQUEST
422 Invalid request body
The route's schema rejected the request. The failing fields are on error.issues, which a custom onError can shape into an API response.
VALIDATION_FAILED
500 Server Error
The handler returned something its own response schema rejects, so this is a bug in the route rather than in the request. The failing fields are on error.issues.
The body
Raised while ctx.body is being parsed.
BODY_TOO_LARGE
413 Request body exceeds the {limit} limit
Raise it with security: { maxBodySize: '10mb' }, or maxBodySize: false to disable the cap. It only bounds what is held in memory; uploaded files stream to uploads and have their own limits.
BODY_INVALID_MULTIPART
400 A multipart/form-data body needs a boundary
The client set Content-Type: multipart/form-data by hand. Let it be set automatically (send a FormData and omit the header) so the boundary is included.
Uploads
Raised while a file is being stored. See uploads for the limits and their defaults.
UPLOAD_NOT_CONFIGURED
500 A file ("{name}") was uploaded but uploads is not configured
Set uploads: './uploads' (or a Bucket) to store files, or uploads: false to ignore file fields on purpose.
UPLOAD_TOO_LARGE
413 File "{name}" is too large ({size} bytes, limit is {limit})
Raise it with uploads: { bucket, maxFileSize: '50mb' }. maxTotalSize bounds one request's files together, and both default to 10mb and 100mb.
UPLOAD_TOO_MANY_FILES
413 Too many files in one request (the limit is {limit})
Raise it with uploads: { bucket, maxFiles: 500 }. It defaults to 100, which bounds how many objects one request can create.
UPLOAD_TOO_SMALL
400 File "{name}" is too small ({size} bytes, minimum is {limit})
Set or lower uploads: { bucket, minSize: '1kb' }.
UPLOAD_TYPE_NOT_ALLOWED
415 File type not allowed for "{name}" (got "{type}", allowed: {allowed})
fileType accepts extensions (.jpg) and MIME types (image/jpeg). It is checked against the file's real format when the bytes identify one, so a mislabelled file is refused even if its name matches.
Authentication
Raised while a credential is read or a login is completed. See Authentication.
AUTH_INVALID_TOKEN
401 Invalid Authorization token
The bearer token did not verify: check the issuer and audience, and that the token has not expired.
AUTH_INVALID_HEADER
401 Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)
The Authorization header must read Bearer <token>, with a space.
AUTH_INVALID_STATE
403 Invalid OAuth state
The OAuth state cookie was missing or did not match. It is signed with secrets, lives for 10 minutes, and needs the callback to be on the same origin as the login.
AUTH_ISSUER_UNREACHABLE
502 Cannot reach the OIDC issuer at <url>
The issuer's discovery document could not be fetched. Check the issuer URL (it must serve /.well-known/openid-configuration) and that this server has network access to it. A cookie or token is never cleared on this error, since the credential itself may be perfectly valid.
AUTH_NO_CODE
400 Missing the OAuth 'code' in the callback URL
The provider redirected back without a code. Check the callback URL registered with the provider matches /auth/callback/<name>.
See also
onError: the hook this page is about.security.maxBodySizeanduploads: the limits behind most of these codes.