Skip to content

Error Reference ​

Every error condition the static asset controller and storage helpers can raise, with cause and fix.

Catalogued errors ​

These carry a machine code a client can branch on, alongside the human message. The code is the stable contract; the message text may be overridden at a throw site to name the offending value.

CodeMessage textHTTP StatusCategory
core.static_asset.bucket_name_invalid"Invalid bucket name"400VALIDATION
core.static_asset.object_name_invalid"Invalid object name or path"400VALIDATION
core.static_asset.folder_path_invalid"Invalid folder path"400VALIDATION
core.static_asset.folder_segment_invalid"Invalid folder path segment"400VALIDATION
core.static_asset.folder_depth_exceeded"Folder path exceeds the maximum depth"400VALIDATION
core.static_asset.file_empty"Empty file content"400VALIDATION
core.static_asset.max_keys_invalid"Invalid maxKeys - expected a positive integer"400VALIDATION
core.static_asset.upload_too_large"File is larger than the maximum allowed"413VALIDATION
core.static_asset.upload_not_authorized"Not allowed to request an upload policy"403VALIDATION
core.static_asset.invalid_commit_token"Invalid commit token"400VALIDATION
core.static_asset.expired_commit_token"Commit token has expired"400VALIDATION
core.static_asset.labels_in_query"Upload labels belong in the request body, not the query"400VALIDATION
core.static_asset.object_key_out_of_scope"Object key is outside the key prefix of this controller"400VALIDATION
core.storage.object_not_found"Object not found"404BUSINESS

The eight core.static_asset.* codes live on StaticAssetErrors; core.storage.object_not_found lives on StorageErrors in the helpers package. Both register with the shared key registry, so messageCode autocompletes.

NOTE

core.request.body_too_large (413) and core.request.body_malformed (400) are not static-asset codes - they live on the kernel's RequestErrors, alongside every other route. See Middlewares Reference.

Error conditions ​

MessageCauseCodeHTTP Status
"Invalid bucket name"bucketName fails isValidBucketName()core.static_asset.bucket_name_invalid400
"Invalid object name or path"objectName fails isValidObjectKey()core.static_asset.object_name_invalid400
"Invalid folder path"Upload's folderPath form field is empty after trimming leading/trailing slashescore.static_asset.folder_path_invalid400
"Folder path exceeds max depth of {n}"Upload's folderPath has more segments than maxFolderDepth (default 2)core.static_asset.folder_depth_exceeded400
"Invalid folder path segment: {segment}"One folderPath segment fails isValidSegment()core.static_asset.folder_segment_invalid400
"Empty file content | name: {originalName}"The uploaded file's buffer is empty after multipart parsing (or after re-reading a disk-spooled file)core.static_asset.file_empty400
"Invalid maxKeys | Expected a positive integer | value: {value}"listObjects's maxKeys query param does not parse to a positive integercore.static_asset.max_keys_invalid400
[getObject] Object not found | bucket: {bucket} | key: {key} (also [getObjectStream], [getStat], [removeObject])No object at that bucket and key. DiskHelper throws it directly; BunS3Helper and MinioHelper map the backend's own failure onto it through asStorageErrorcore.storage.object_not_found404
Object not found | key: {key}The key does not start with controller.keyPrefix - answered before any storage call, so a caller learns nothing about a key outside its scopecore.storage.object_not_found404
"Object key is outside the key prefix of this controller"A resolveObjectName or extra.normalizeNameFn key falls outside controller.keyPrefixcore.static_asset.object_key_out_of_scope400
[upload] Bucket does not exist | name: {bucket}helper.upload() found no matching bucket via hasBucket()-400 (default)
[upload] Invalid original file nameWith no naming hook, a file's originalName becomes the key and fails isValidSegment() (single segment: no /). The static-asset controller with resolveObjectName or extra.normalizeNameFn set relaxes this - originalName is then metadata, checked by the looser isValidOriginalName() (not blank, at most 255 characters, no control character or lone surrogate). One exception: controller.keyPrefix set with neither hook (the component's own default naming under a scope) keeps the strict, single-segment rule - a keyPrefix alone does not relax it-400 (default)
[upload] Invalid folder path | depth: {n} | max: {m}helper.upload()'s own check found more folderPath segments than maxFolderDepth allows-400 (default)
[upload] Invalid folder pathhelper.upload()'s own path check failed for any other reason-400 (default)
[upload] Invalid file size | size: {size}A file's size is undefined, null, or negative-400 (default)
[upload] Invalid normalized object name | name: {name}The key returned by normalizeNameFn fails isValidObjectKey()-400 (default)
[createBucket] Invalid name to create bucket!createBucket() called with a name failing isValidBucketName() - only reachable calling a helper directly, the controller validates first-400 (default)
[createBucket] Bucket already exists | name: {name}DiskHelper.createBucket() called with a name that already exists on disk. BunS3Helper throws whatever the S3 endpoint returns for the same case instead-400 (default)
[removeBucket] Invalid name to remove bucket!removeBucket() called with a name failing isValidBucketName() - only reachable calling a helper directly-400 (default)
[removeBucket] Bucket does not exist | name: {name}DiskHelper.removeBucket() - no directory at that bucket name-400 (default)
[removeBucket] Bucket is not empty | name: {name}DiskHelper.removeBucket() - the bucket directory still has files in it-400 (default)
[parseMultipartBody] storage: {storage} | Invalid storage type | Valids: ['memory', 'disk']extra.parseMultipartBody.storage set to something other than 'memory'/'disk' - a configuration error, not user input-400 (default)

NOTE

Entries marked "default" pass no explicit statusCode to getError(). ApplicationError defaults statusCode to 400 in that case.

Name validation rules ​

Bucket names are validated with isValidBucketName(), which is isValidSegment() applied to bucket.name (single segment, no path separators). Object keys, which may include folder segments (for example 2026/uploads/report.pdf), are validated with isValidObjectKey() - every segment still runs through isValidSegment().

PatternExampleReason
Path traversal../etc/passwdContains .., /, or \
Hidden files.hiddenStarts with .
Shell metacharactersfile;rm -rf /Contains ;, |, &, $, `, <, >, {, }, [, ], !, or #
Control charactersfile\ninjectedContains \n, \r, or \0
Long names256+ charactersExceeds the 255-character limit
Empty names"", " "Empty or whitespace-only
Double slashes (key only)a//b.pngisValidObjectKey() rejects empty segments
Excess folder depth (key only)a/b/c/d.png with maxFolderDepth: 2folderDepth = segments.length - 1 exceeds the limit
Long keys (key only)1025+ charactersExceeds the 1024-character normalized-key limit

Troubleshooting ​

"Invalid bucket name" / "Invalid object name or path" ​

  • Cause: the name fails the rules above.
  • Fix: strip path separators, leading dots, shell metacharacters, and control characters. Keep names under 255 characters (1024 for a full object path). Never send an empty or whitespace-only value.
typescript
// Wrong - contains a path separator, rejected by isValidBucketName()
await fetch('/assets/buckets/user/uploads', { method: 'POST' });

// Right - one segment
await fetch('/assets/buckets/user-uploads', { method: 'POST' });

"Folder path exceeds max depth" / "Invalid folder path segment" ​

  • Cause: the upload's folderPath form field has more segments than maxFolderDepth allows (default 2), or one segment fails isValidSegment().
  • Fix: flatten the folder structure, or raise the limit via extra.maxFolderDepth when registering the backend.
typescript
{
  storage: StaticAssetStorageTypes.DISK,
  helper: new DiskHelper({ basePath: './uploads' }),
  extra: { maxFolderDepth: 4 },
}

"Empty file content" ​

  • Cause: the uploaded file's buffer is empty - either the client sent a zero-byte file field, or parseMultipartBody produced no readable content.
  • Fix: verify the FormData field actually carries file bytes before submitting. A zero-byte file is rejected - this is not a size-limit issue.

"Invalid maxKeys" ​

  • Cause: listObjects's maxKeys query string does not parse to a positive integer via Number(maxKeys) + Number.isInteger() - for example maxKeys=abc or maxKeys=-1.
  • Fix: only send positive integer strings, or omit the parameter entirely.
typescript
url.searchParams.set('maxKeys', '50'); // OK
// url.searchParams.set('maxKeys', 'all'); // 400

A key that should exist answers "Object not found" (404) ​

  • Cause: the controller has controller.keyPrefix set, and the requested key does not start with it. The route answers exactly as it would for a real miss, before calling storage, so a caller learns nothing about a key it does not own.
  • Fix: request the key with its full prefix, for example tenant-a/photos/2026/f.jpg on a controller scoped to tenant-a.

"Object key is outside the key prefix of this controller" ​

  • Cause: controller.keyPrefix is set, and resolveObjectName or extra.normalizeNameFn returned a key outside it. The component never rewrites the key into scope - the application may have recorded the out-of-scope key elsewhere.
  • Fix: have the hook build keys under the prefix, or drop controller.keyPrefix if the hook already scopes keys on its own.

"[upload] Invalid original file name" under controller.keyPrefix ​

  • Cause: controller.keyPrefix is set with neither resolveObjectName nor extra.normalizeNameFn - the component's own default naming under a scope. That path keeps the strict, single-segment rule a name has without any scope; keyPrefix alone does not relax it.
  • Fix: rename the file (drop the /), or set resolveObjectName/extra.normalizeNameFn to build the key yourself, which frees the original name to be metadata.

"keyPrefix needs controller.bucket" ​

  • Cause: controller.keyPrefix is set without controller.bucket (bucket-in-URL mode). With no configured bucket, the four bucket-management routes cannot be scoped, so keyPrefix would isolate only part of the controller's surface - the controller refuses to register rather than isolate less than its name promises.
  • Fix: add controller.bucket, the same requirement directUpload already has.

"[upload] Bucket does not exist" ​

  • Cause: helper.upload() checked hasBucket() and found no match - the bucket was never created, or was deleted between requests.
  • Fix: create the bucket first with POST /buckets/:bucketName, or check GET /buckets/:bucketName before uploading.

"Bucket already exists" ​

  • Cause: POST /buckets/:bucketName was called for a bucket that already exists. createBucket() throws rather than returning null - it does not silently succeed on a duplicate.
  • Fix: check GET /buckets/:bucketName first, or treat the throw as "already there" and continue.

Controller not registering / no routes appear ​

  • Cause: STATIC_ASSET_COMPONENT_OPTIONS was never bound, or was bound to {} - the component's default. binding() iterates the options object and produces zero controllers for an empty map, without throwing.
  • Fix: bind a non-empty options object before this.component(StaticAssetComponent):
typescript
this.bind<TStaticAssetsComponentOptions>({
  key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
}).toValue({
  [uniqueKey]: {
    controller: { name: 'AssetController', basePath: '/assets' },
    storage: StaticAssetStorageTypes.DISK,
    helper: new DiskHelper({ basePath: './uploads' }),
  },
});
this.component(StaticAssetComponent);

Files not uploading (DiskHelper) ​

  • Cause: the process lacks filesystem permissions on basePath, or the disk is out of space. DiskHelper's constructor already creates basePath if it does not exist, so a missing directory is rarely the issue.
  • Fix: verify the process has read/write permissions to the target path and enough free space.

Files not uploading (BunS3Helper) ​

  • Cause: endpoint connectivity or authentication failure.
  • Fix:
    • Confirm the endpoint is reachable from this process.
    • Verify accessKey/secretKey and that region matches the bucket.
    • Behind a private endpoint, set endpoint.public so signed URLs carry a host a browser can reach.

Large file uploads failing or timing out ​

  • Cause: parseMultipartBody: { storage: 'memory' } (the default) buffers the entire file in process memory before writing it to the backend.
  • Fix: switch to disk-based spooling:
typescript
extra: {
  parseMultipartBody: { storage: 'disk', uploadDir: './tmp/uploads' },
}

BunS3Helper fails to construct or import ​

  • Cause: running on Node.js. BunS3Helper imports S3Client from the bun builtin module, which only resolves under the Bun runtime.
  • Fix: run the app under Bun, or use DiskHelper outside it.

See also ​

  • Overview - quick start, imports, and common configuration tasks
  • Usage & Examples - task-oriented walkthroughs for every endpoint and MetaLink setup
  • Full Reference - controller factory, IStorageHelper interface, MetaLink schema, internals

The size ceiling, and the two token errors ​

upload_too_large is one code for both upload paths: over extra.maxBytes on POST {base}/objects, and over directUpload.maxBytes when a policy is requested. A second code would have been the same condition wearing two names.

The ordinary route checks twice. The request's content-length is read before the body is spooled, so an oversized upload is refused without first landing on disk; then each parsed file is checked against its actual byte length, which is the authoritative one - a multipart envelope is larger than the files inside it, and a declared length is whatever the client declared.

invalid_commit_token and expired_commit_token are both 400 and neither is a 404. A caller learns only that the token is no good, never whether the key it names exists - the route is not a name prober, and a forged token fails before any storage call, so timing leaks nothing either.