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-sdkpnpm add files-sdkyarn add files-sdkbun add files-sdkWhen 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. 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",
});
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:
ReadableStreambodies are buffered in memory before a single PUT (a lone PUT needs aContent-Length, and single-request uploads cap at 5 GB). Inside a Worker that buffering counts against the isolate’s memory limit.multipartand resumable (control) uploads throw instead of engaging the S3 multipart API.- Bulk deletes fan out as per-key
delete()calls instead of batchedDeleteObjectsrequests. 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;maxSizethrows because enforcing it 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
onProgressreporting falls back to the SDK’s generic reporting. - No provider-native conditional operations.
files.rawis the aws4fetchAwsClient, not anS3Client.
Options
S3FetchAdapterOptions: Cannot read properties of undefined (reading 'ESNext')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 type 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 throws (no presigned POST policy). |
| conditional operations | ❌ | Not advertised on this engine. |