Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

S3 (fetch)

AWS S3 and any S3-compatible bucket over a SigV4-signed fetch engine - no @aws-sdk/* packages, built for Cloudflare Workers and other edge runtimes.

Installation

This adapter has no extra peer dependencies. It signs requests with aws4fetch (~2.5 KB gzipped, Web Crypto only), so nothing from @aws-sdk/* is installed or bundled:

npm install files-sdk
pnpm add files-sdk
yarn add files-sdk
bun add files-sdk
nub add files-sdk
aube add files-sdk

When to use it

files-sdk/s3 and the S3-compatible wrappers built on it (MinIO, Tigris, Wasabi, …) import @aws-sdk/client-s3. On a browser-targeted bundle that SDK’s XML parser needs a DOMParser, which Cloudflare Workers don’t provide - every list() or error-body parse throws DOMParser is not defined at runtime. s3Fetch() is the same engine that backs r2()’s fetch client, pointed at an endpoint of your choosing, for exactly those runtimes.

Outside Workers and edge runtimes prefer files-sdk/s3: it carries the full surface (multipart and resumable uploads, batched deletes, byte-level progress, the AWS credential chain) that this engine trades away for size.

Usage

import { Files } from "files-sdk";
import { s3Fetch } from "files-sdk/s3-fetch";

export default {
  async fetch(request: Request, env: Env) {
    const files = new Files({
      adapter: s3Fetch({
        bucket: "uploads",
        endpoint: "https://s3.us-east-1.amazonaws.com",
        region: "us-east-1",
        accessKeyId: env.AWS_ACCESS_KEY_ID,
        secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
      }),
    });
    // ...
  },
};

Credentials are static: pass accessKeyId + secretAccessKey (and sessionToken for temporary credentials), or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN where a process.env exists. AWS_SESSION_TOKEN is only read when both keys also come from the environment, so keys you pass explicitly never pick up a token that belongs to another identity (a Lambda role or SSO session, say). There is no AWS credential chain - no IAM role, shared profile, or SSO resolution - because that chain lives in the SDK this adapter avoids.

Addressing

Requests default to virtual-hosted style (https://<bucket>.<endpoint host>/<key>). Set forcePathStyle: true for services without per-bucket DNS - MinIO, LocalStack, most self-hosted gateways - and for bucket names containing dots, which break virtual-hosted TLS:

s3Fetch({
  bucket: "uploads",
  endpoint: "http://localhost:9000",
  forcePathStyle: true,
  accessKeyId: "minioadmin",
  secretAccessKey: "minioadmin",
});

Get this wrong against such a service and the first request fails with a NotFound whose message names the wrong bucket: with no per-bucket DNS the server reads the key’s first folder as the bucket name. The error message points at forcePathStyle when that happens, but only on responses that carry an error body. A HEAD response has none, so head() rejects with a bare NotFound and exists() returns false for every key. If exists() reports everything missing on a new setup, check forcePathStyle first. For MinIO specifically, prefer minio({ client: "fetch" }) - the same engine with path style, region, and error labels already defaulted.

Only the endpoint’s origin is used; a path component is dropped. Front the service with a hostname rather than a route prefix.

Trade-offs

Same coverage and limits as the R2 fetch client: upload, download (including ranges), head, exists, delete, list (including delimiter folding), server-side copy, presigned url(), and signedUploadUrl() are all covered. Against files-sdk/s3:

  • ReadableStream bodies are buffered in memory before a single PUT (a lone PUT needs a Content-Length, and single-request uploads cap at 5 GB). Inside a Worker that buffering counts against the isolate’s memory limit.
  • multipart and resumable (control) uploads throw instead of engaging the S3 multipart API.
  • Bulk deletes fan out as per-key delete() calls instead of batched DeleteObjects requests. On Workers each call is a subrequest, so chunk large bulk deletes to stay under the per-invocation subrequest cap.
  • signedUploadUrl() returns a presigned PUT; maxSize and a positive minSize throw because enforcing either needs a presigned POST policy this engine doesn’t implement. Enforce upload caps at your application gateway instead.
  • Keys containing . or .. path segments are rejected - URL normalization would silently sign a request for a different key.
  • Byte-level onProgress reporting falls back to the SDK’s generic reporting.
  • No provider-native conditional operations.
  • files.raw is the aws4fetch AwsClient, not an S3Client.

S3’s own limits apply here too: presigned URLs live at most 7 days (a longer expiresIn throws), and copy() is a single CopyObject request up to 5 GiB, past which it switches to a server-side multipart copy (UploadPartCopy).

Options

PropType
bucketstring

Bucket name. All operations are scoped to it.

Typestring
endpointstring

Service endpoint origin, e.g. `https://s3.us-east-1.amazonaws.com`, `http://localhost:9000`, or `https://t3.storage.dev`. Only the origin is used — a path component is dropped, so front the service with a host, not a route prefix.

Typestring
region?string

SigV4 signing region. Falls back to `AWS_REGION` / `AWS_DEFAULT_REGION`, then `us-east-1`. Every S3-compatible service needs *some* region in the signature; most ignore it for routing.

Typestring
accessKeyId?string

Static credentials. Falls back to `AWS_ACCESS_KEY_ID`; required if that env var isn't set. No credential chain — there is no IAM role, shared profile, or SSO resolution here, which is what keeps this engine `@aws-sdk/*`-free.

Typestring
secretAccessKey?string

Static credentials. Falls back to `AWS_SECRET_ACCESS_KEY`; required if that env var isn't set.

Typestring
sessionToken?string

Session token for temporary credentials. Falls back to `AWS_SESSION_TOKEN`, but only when `accessKeyId` and `secretAccessKey` also come from the environment. Explicit keys never pick up an env token.

Typestring
forcePathStyle?boolean

Use path-style addressing (`https://endpoint/bucket/key`) instead of the default virtual-hosted style (`https://bucket.endpoint/key`). Set it for MinIO and other self-hosted services without per-bucket DNS, and for bucket names containing dots (virtual-hosted TLS breaks on them).

Typeboolean
publicBaseUrl?string

Origin used to build URLs from `url()`. When set, `url(key)` returns `${publicBaseUrl}/${key}` — for a public bucket policy or a CDN in front of it. When unset, `url()` falls back to a presigned GetObject. An explicit `url(key, { expiresIn })` or `responseContentDisposition` still presigns, since a permanent link can't expire or carry the override.

Typestring
defaultUrlExpiresIn?number

Default expiry, in seconds, for the presigned URLs returned by `url()` when no per-call `expiresIn` is given. Defaults to 3600 (1 hour).

Typenumber
fetch?(request: Request) => Promise<Response>

Override the `fetch` implementation used for every request — for tests, or runtimes that hand out a bound/instrumented fetch. Defaults to `globalThis.fetch`.

Type(request: Request) => Promise<Response>

Compatibility

Method Status Notes
upload ⚠️ Single PUT - ReadableStream bodies are buffered in memory first, and multipart / resumable control uploads throw.
download ✅
delete ✅ Bulk deletes fan out per key (no batched DeleteObjects).
list ⚠️ The S3 list API returns no per-object Content-Type, so contentType is inferred from the key’s extension (application/octet-stream when unknown). Use head() for the stored value.
search ✅
head ✅
exists ✅
copy ✅
url ✅
signedUploadUrl ⚠️ PUT URL only - maxSize and a positive minSize throw (no presigned POST policy).
conditional operations ❌ Not advertised on this engine.

Last updated on

Was this page helpful?