S3
Lightweight, dependency-free fake of AWS S3 that speaks the real S3 REST API (XML wire protocol), so application code using @aws-sdk/client-s3 can run against it with zero cost and zero side effects.
| Key | Value |
|---|---|
| Port | 4566 |
| Protocol | AWS S3 REST API (HTTP + XML) |
| Compatible client | @aws-sdk/client-s3 (v3) |
| Size | ~96 KB |
| Startup | < 100ms |
| State | In-memory, ephemeral, resettable |
Quick Start
Start the server:
import { S3Server } from "./services/s3/src/server.js";
const server = new S3Server(4566);
await server.start();
// ... use it ...
await server.stop();
Connect with the real AWS SDK client:
import {
S3Client,
CreateBucketCommand,
PutObjectCommand,
GetObjectCommand,
} from "@aws-sdk/client-s3";
const s3 = new S3Client({
region: "us-east-1",
endpoint: "http://127.0.0.1:4566",
forcePathStyle: true, // recommended for the fake
credentials: { accessKeyId: "parlel", secretAccessKey: "parlel" },
});
await s3.send(new CreateBucketCommand({ Bucket: "my-bucket" }));
await s3.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "hello.txt", Body: "hello world" }));
const obj = await s3.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "hello.txt" }));
console.log(await obj.Body.transformToString()); // "hello world"
Addressing
Both addressing styles are supported:
- Path-style (recommended):
http://127.0.0.1:4566/{bucket}/{key}— setforcePathStyle: true. - Virtual-hosted-style:
http://{bucket}.s3.amazonaws.com/{key}— resolved from theHostheader.
Authentication
SigV4 signatures are accepted but not verified (any credentials work). This matches LocalStack-style local development.
Access via Parlel Sandbox
S3 is an HTTP service, so inside a Parlel sandbox it is exposed directly at its
own Daytona preview URL (not through the MCP parlel_execute tool, which is for
TCP database services). Point any @aws-sdk/client-s3 client — or plain
HTTP — at the preview URL from the sandbox's Connect panel:
# the preview token comes from the Connect panel / GET /connection
curl -X PUT "$S3_URL/my-bucket" -H "x-daytona-preview-token: $TOKEN"
curl -X PUT "$S3_URL/my-bucket/hello.txt" -H "x-daytona-preview-token: $TOKEN" --data "hello world"
curl "$S3_URL/my-bucket/hello.txt" -H "x-daytona-preview-token: $TOKEN" # -> hello world
Internal / Health Endpoints
These are not part of the S3 API; they support local tooling.
| Method | Path | Description |
|---|---|---|
GET | /_parlel/health | Returns { "status": "ok", "service": "s3", "buckets": N } |
POST | /_parlel/reset | Clears all in-memory state |
You can also reset programmatically via server.reset().
Implemented Operations
Service
| Operation | SDK Command | Notes |
|---|---|---|
| ListBuckets | ListBucketsCommand | GET / |
Bucket — core
| Operation | SDK Command | Notes |
|---|---|---|
| CreateBucket | CreateBucketCommand | Validates bucket name; idempotent re-create in us-east-1 returns 200, BucketAlreadyOwnedByYou (409) in other regions |
| DeleteBucket | DeleteBucketCommand | BucketNotEmpty if it has live objects |
| HeadBucket | HeadBucketCommand | 200 / 404 + x-amz-bucket-region |
| GetBucketLocation | GetBucketLocationCommand | Empty constraint for us-east-1 |
Bucket — configuration
| Operation | SDK Command |
|---|---|
| GetBucketVersioning / PutBucketVersioning | GetBucketVersioningCommand / PutBucketVersioningCommand |
| GetBucketTagging / PutBucketTagging / DeleteBucketTagging | GetBucketTaggingCommand / PutBucketTaggingCommand / DeleteBucketTaggingCommand |
| GetBucketCors / PutBucketCors / DeleteBucketCors | GetBucketCorsCommand / PutBucketCorsCommand / DeleteBucketCorsCommand |
| GetBucketPolicy / PutBucketPolicy / DeleteBucketPolicy | GetBucketPolicyCommand / PutBucketPolicyCommand / DeleteBucketPolicyCommand |
| GetBucketAcl / PutBucketAcl | GetBucketAclCommand / PutBucketAclCommand |
| GetBucketLifecycleConfiguration / PutBucketLifecycleConfiguration / DeleteBucketLifecycle | GetBucketLifecycleConfigurationCommand / PutBucketLifecycleConfigurationCommand / DeleteBucketLifecycleCommand |
| GetBucketEncryption / PutBucketEncryption / DeleteBucketEncryption | GetBucketEncryptionCommand / PutBucketEncryptionCommand / DeleteBucketEncryptionCommand |
| GetBucketWebsite / PutBucketWebsite / DeleteBucketWebsite | GetBucketWebsiteCommand / PutBucketWebsiteCommand / DeleteBucketWebsiteCommand |
Configuration documents (CORS, lifecycle, encryption, website) are stored and returned verbatim so the SDK round-trips them faithfully.
Object — core
| Operation | SDK Command | Notes |
|---|---|---|
| PutObject | PutObjectCommand | MD5 ETag, user metadata, x-amz-tagging, Content-MD5 validation (BadDigest) |
| GetObject | GetObjectCommand | Range requests (206 / InvalidRange), conditional headers, response-* overrides |
| HeadObject | HeadObjectCommand | Metadata only; 404 on miss |
| DeleteObject | DeleteObjectCommand | Idempotent; inserts a delete marker when versioning is enabled |
| DeleteObjects | DeleteObjectsCommand | Batch delete, supports Quiet; empty/malformed body → MalformedXML (400) |
| CopyObject | CopyObjectCommand | x-amz-copy-source, COPY/REPLACE metadata & tagging directives |
Object — listing
| Operation | SDK Command | Notes |
|---|---|---|
| ListObjects (v1) | ListObjectsCommand | prefix, delimiter, marker, max-keys, CommonPrefixes |
| ListObjectsV2 | ListObjectsV2Command | continuation-token, start-after, fetch-owner, KeyCount |
| ListObjectVersions | ListObjectVersionsCommand | Versions + delete markers, IsLatest |
Object — tagging / ACL / attributes
| Operation | SDK Command |
|---|---|
| GetObjectTagging / PutObjectTagging / DeleteObjectTagging | GetObjectTaggingCommand / PutObjectTaggingCommand / DeleteObjectTaggingCommand |
| GetObjectAcl / PutObjectAcl | GetObjectAclCommand / PutObjectAclCommand |
| GetObjectAttributes | GetObjectAttributesCommand |
Multipart uploads
| Operation | SDK Command | Notes |
|---|---|---|
| CreateMultipartUpload | CreateMultipartUploadCommand | Returns UploadId |
| UploadPart | UploadPartCommand | Per-part MD5 ETag; part number 1–10000 |
| CompleteMultipartUpload | CompleteMultipartUploadCommand | Multipart ETag md5(part-md5s)-N; validates order & parts |
| AbortMultipartUpload | AbortMultipartUploadCommand | Discards the upload |
| ListParts | ListPartsCommand | Lists uploaded parts |
| ListMultipartUploads | ListMultipartUploadsCommand | Lists in-progress uploads |
Surface coverage
This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.
Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.
| Feature | Status |
|---|---|
| Buckets: create / delete / list / head / location | ✅ Supported |
CreateBucket idempotent re-create (us-east-1 → 200, other regions → 409) | ✅ Supported |
| Objects: put / get / head / delete / copy | ✅ Supported |
Batch delete (DeleteObjects) | ✅ Supported |
Batch delete validation (empty/malformed body → MalformedXML) | ✅ Supported |
| Listing v1 + v2 with prefix/delimiter/pagination | ✅ Supported |
Range + conditional GET (If-Match, If-None-Match, If-(Un)Modified-Since) | ✅ Supported |
User metadata (x-amz-meta-*) | ✅ Supported |
| Object & bucket tagging | ✅ Supported |
| Object & bucket ACL (canned, static owner) | ◐ Accepted (canned grant returned; ACLs not enforced) |
| Versioning + delete markers + version listing | ✅ Supported |
| Multipart uploads (full lifecycle) | ✅ Supported |
Multipart EntityTooSmall enforcement (non-final part < 5 MiB) | ✓ By design — not enforced so tests can use tiny parts |
| Bucket config: CORS / policy / lifecycle / encryption / website | ◐ Stored & round-tripped (not validated) |
| GetObjectAttributes | ✅ Supported |
| Virtual-hosted & path-style addressing | ✅ Supported |
Error envelope (Code / Message / Resource / RequestId / HostId) | ✅ Supported — RequestId/HostId match the x-amz-request-id / x-amz-id-2 headers |
| List pagination for versions / multipart-uploads / parts | ✓ By design — single page (IsTruncated=false) |
| SigV4 signature verification | ◐ Accepted but not enforced |
| Server-side encryption (actual crypto) | ✓ By design — config stored, data is not encrypted |
| POST form (browser) uploads | ⟳ Roadmap — NotImplemented (501) |
| Replication, logging, notification, inventory, analytics, metrics, object-lock, request-payment, accelerate, public-access-block, ownership-controls, intelligent-tiering, S3 Select | ⟳ Roadmap — NotImplemented (501) |
| Presigned URL signature validation | ◐ URL routing works; signature not checked |
Error Codes & Shapes
Errors use the standard S3 XML envelope:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
<Key>missing.txt</Key>
<Resource>/my-bucket/missing.txt</Resource>
<RequestId>…</RequestId>
<HostId>…</HostId>
</Error>
RequestId and HostId in the body match the x-amz-request-id and x-amz-id-2 response headers, just like the real S3 API.
| Code | HTTP | When |
|---|---|---|
NoSuchBucket | 404 | Bucket does not exist |
NoSuchKey | 404 | Object/version does not exist |
NoSuchUpload | 404 | Multipart upload id not found |
NoSuchTagSet | 404 | Bucket tagging requested but unset |
NoSuchCORSConfiguration | 404 | Bucket CORS requested but unset |
NoSuchBucketPolicy | 404 | Bucket policy requested but unset |
NoSuchLifecycleConfiguration | 404 | Lifecycle requested but unset |
NoSuchWebsiteConfiguration | 404 | Website config requested but unset |
ServerSideEncryptionConfigurationNotFoundError | 404 | Encryption requested but unset |
InvalidBucketName | 400 | Bucket name fails S3 naming rules |
BucketAlreadyOwnedByYou | 409 | Re-creating an existing bucket outside us-east-1 (in us-east-1 re-create is idempotent → 200) |
MalformedXML | 400 | DeleteObjects body is empty or has no <Object> entries |
BucketNotEmpty | 409 | Deleting a bucket with live objects |
BadDigest | 400 | Content-MD5 does not match the body |
InvalidRange | 416 | Unsatisfiable Range header |
InvalidPart | 400 | Completing multipart with a missing/mismatched part |
InvalidPartOrder | 400 | Parts not in ascending order |
PreconditionFailed | 412 | If-Match / If-Unmodified-Since failed |
NotModified | 304 | If-None-Match / If-Modified-Since matched |
NotImplemented | 501 | Operation/sub-resource not implemented |
MethodNotAllowed | 405 | Method not valid for the resource |
InternalError | 500 | Unexpected server error |
Environment Variables
The manifest advertises these defaults for clients/tooling:
| Variable | Default |
|---|---|
AWS_ACCESS_KEY_ID | parlel |
AWS_SECRET_ACCESS_KEY | parlel |
AWS_REGION | us-east-1 |
AWS_ENDPOINT_URL_S3 | http://127.0.0.1:4566 |
AWS_S3_FORCE_PATH_STYLE | true |
Configuration — test.env
Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.
AWS_ACCESS_KEY_ID=parlel
AWS_SECRET_ACCESS_KEY=parlel
AWS_REGION=us-east-1
AWS_ENDPOINT_URL_S3=http://parlel-bridge:4566
AWS_S3_FORCE_PATH_STYLE=true
<!-- parlel:testenv:end -->