overcast local AWS emulator

Service Reference

S3 — Simple Storage Service

S3 uses a REST-style XML API. Requests are routed by virtual-hosted-style (bucket.s3.localhost:4566) or path-style (localhost:4566/bucket) depending on the S3_ADDRESSING_STYLE...

S3 — Simple Storage Service

AWS docs: https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html

S3 uses a REST-style XML API. Requests are routed by virtual-hosted-style (bucket.s3.localhost:4566) or path-style (localhost:4566/bucket) depending on the S3_ADDRESSING_STYLE environment variable (default: path-style for local dev simplicity).


Known limitations

  • Virtual-hosted-style addressing recognises the bucket name in the Host header for any of: bucket.s3.<anything> (including bucket.s3.<region>.<anything> and the legacy dash dialect bucket.s3-<region>.<anything>), and the bare bucket.<base> form for localhost, the built-in wildcard-DNS domains (localhost.overcast.sh, localhost.localstack.cloud, localhost.floci.io) and a configured OVERCAST_HOSTNAME. localhost.overcast.sh is the recommended setting — see networking.md. In the bare form, a bucket portion carrying a reserved service label (execute-api, lambda-url, appsync-api, appsync-realtime-api, cloudfront) as a second-or-later dot segment is a service address rather than a bucket; use path-style or the .s3. form for those. The bucket name is everything before the first .s3./.s3- label. A bucket name that itself contains the literal substring .s3. is ambiguous under this rule and will be mis-split — real AWS has the same limitation and recommends against dots in bucket names used with virtual-hosted-style/TLS for this and other reasons.
  • Presigned URLs are supported: the SigV4 middleware validates presigned URL signatures against IAM access keys and STS session credentials. The signer secret is resolved from the user’s SecretAccessKey stored at key creation time. When OVERCAST_SIGV4_VALIDATE is enabled, invalid or expired presigned URLs return 403 InvalidSignatureException.
  • SSE (server-side encryption) headers are accepted and echoed but no actual encryption is performed.
  • S3 Select is out of scope for v1.

Bucket configuration behaviour

Versioning: version ids, delete markers and suspended buckets

A bucket has three versioning states, and the third is the one worth reading carefully.

  • Unversioned (the initial state, no VersioningStatus). Each key holds one object, a write replaces it, and a delete removes it. Nothing else is stored, and ListObjectVersions reports each object as its key’s null version — which is what AWS reports for such a bucket too.
  • Enabled. Every write mints a new version id, returned in x-amz-version-id, and the previous version becomes noncurrent rather than being replaced. A delete with no versionId removes nothing: it adds a delete marker that becomes the current version. A delete that names a versionId removes that one version permanently, and if it was the current one the newest surviving version takes its place.
  • Suspended. Not “off”. Versions already recorded are kept and stay addressable; new writes become the key’s single null version, replacing any previous null version; and a delete still leaves a delete marker behind, with version id null. There is no transition back to unversioned, on AWS or here.

Objects stored before a bucket was versioned — including objects persisted by an Overcast build that predates version history — are the null version of their key. They stay readable, appear in ListObjectVersions as VersionId=null, and can be addressed with ?versionId=null. Nothing has to be migrated by hand.

Reads distinguish AWS’s two “not there” answers:

RequestAnswer
GET/HEAD of a key whose current version is a delete marker404 NoSuchKey with x-amz-delete-marker: true
GET/HEAD naming a delete marker’s own versionId405 MethodNotAllowed with x-amz-delete-marker: true and Allow: DELETE
GET/HEAD naming a versionId that does not exist404 NoSuchVersion

ListObjects and ListObjectsV2 never show delete markers or noncurrent versions — only ListObjectVersions does. It returns entries in AWS’s order, keys ascending and, within a key, most recently stored first, with Version and DeleteMarker elements interleaved. It supports prefix, delimiter, max-keys and resumable key-marker/version-id-marker pagination; a version-id-marker without a key-marker, or one that names no version of the marker key, is refused with 400 InvalidArgument as AWS refuses it.

A bucket whose keys are all delete-marked is not empty: DeleteBucket answers BucketNotEmpty until the markers and the versions under them are removed, which is the AWS behaviour that makes the SDKs’ ListObjectVersionsDeleteObjects cleanup loop necessary.

Version ids are opaque, URL-safe, fixed-width tokens. Nothing about their internal structure is part of the contract — treat them exactly as you would an AWS version id.

Lifecycle: version-aware actions

On a versioned bucket the same rules mean different things, and Overcast’s sweeper applies AWS’s meanings:

  • Expiration on the current version of a versioned object adds a delete marker rather than deleting anything. On an unversioned bucket it still deletes outright.
  • NoncurrentVersionExpiration permanently removes versions that have been noncurrent for NoncurrentDays. A version’s noncurrent clock starts when the version that replaced it was written, not when the version itself was written. NewerNoncurrentVersions retains that many newer noncurrent versions before the action applies to the rest.
  • NoncurrentVersionTransition marks noncurrent versions with a storage class on the same eligibility rules. It composes with the bucket’s default minimum transition size below rather than bypassing it: a version under 128 KB is skipped for the same reasons a current object would be. Delete markers are never transitioned — there are no bytes to move.
  • ExpiredObjectDeleteMarker removes a delete marker that has become its key’s only version, clearing the tombstone left behind once everything it hid has expired away. AWS refuses it alongside Days or Date in the same Expiration, and so does Overcast (400 InvalidRequest).

Expiration wins over transition for a given version, as on AWS. Every action runs on the injected clock through the hourly sweeper, so a test can reach them by advancing a mock clock rather than waiting.

Lifecycle: default minimum transition size

PutBucketLifecycleConfiguration accepts the x-amz-transition-default-minimum-object-size header and echoes it on its own response and on GetBucketLifecycleConfiguration, as AWS does. Both documented values are supported; omitting the header stores nothing and reads back as AWS’s default, all_storage_classes_128K.

The setting is not merely round-tripped — the lifecycle sweeper applies it:

  • all_storage_classes_128K (the default) — an object smaller than 128 KB is not transitioned to any storage class.
  • varies_by_storage_class — an object smaller than 128 KB still transitions to GLACIER (Glacier Flexible Retrieval) or DEEP_ARCHIVE (Glacier Deep Archive); every other class keeps the 128 KB floor.
  • A rule whose own Filter sets ObjectSizeGreaterThan or ObjectSizeLessThan opts out of the default entirely, because on AWS “custom filters always take precedence over the default transition behavior”.

A value outside the two documented ones is refused with 400 InvalidArgument rather than stored, so a stored configuration cannot claim a transition behaviour the sweeper would not apply. Every PutBucketLifecycleConfiguration replaces the configuration wholesale, so a later call that omits the header returns the bucket to the default.

CloudFormation’s AWS::S3::Bucket.LifecycleConfiguration.TransitionDefaultMinimumObjectSize is dispatched through this header; S3 still owns the validation.

Website: redirects and routing rules

PutBucketWebsite accepts the whole WebsiteConfiguration document, and GetBucketWebsite returns it unchanged:

  • RedirectAllRequestsTo with its HostName and optional Protocol.
  • IndexDocument and ErrorDocument.
  • RoutingRules, each with an optional Condition (HttpErrorCodeReturnedEquals, KeyPrefixEquals) and a Redirect (HostName, HttpRedirectCode, Protocol, ReplaceKeyPrefixWith, ReplaceKeyWith).

AWS’s constraints are enforced rather than assumed: RedirectAllRequestsTo cannot be combined with any other element, a configuration must carry either it or an IndexDocument, Protocol must be http or https, an IndexDocument Suffix must be non-empty and slash-free, a Redirect must name at least one destination field, ReplaceKeyWith and ReplaceKeyPrefixWith are mutually exclusive, and a Condition must carry at least one predicate. Anything refused returns 400 InvalidArgument and leaves the previous configuration in place. Each PutBucketWebsite replaces the whole document, and DeleteBucketWebsite removes it.

Two deliberate boundaries:

  • HttpRedirectCode values are not validated. Real S3 rejects a code that is not a valid HTTP redirect status; the exact error it returns is not in the API model, so Overcast stores the value rather than inventing a rejection.
  • Overcast serves no S3 website endpoint. The configuration is stored and returned faithfully, so a stack that deploys one deploys it, but no request is actually redirected or served an index document.

CloudFormation’s AWS::S3::Bucket.WebsiteConfiguration dispatches all four elements through PutBucketWebsite, translating its own RedirectRule and RoutingRuleCondition spellings into the API’s Redirect and Condition.

Notifications: EventBridge

NotificationConfiguration carries EventBridgeConfiguration alongside the queue, topic and Lambda destinations. AWS models it as an element with no content, so presence is the whole signal: while it is set the bucket sends every object event to the default event bus, with no event-type selection and no key filter. Overcast preserves it next to the other destinations, omits it from GetBucketNotificationConfiguration when it is not set (emitting an empty element would turn delivery on for any client that read the configuration back and put it again), and clears it when a later Put omits it.

Object events are published through EventBridge’s own delivery path, so rule patterns, input transformers, retries and dead-letter queues behave exactly as they do for PutEvents. The envelope follows AWS’s documented S3 event:

{
  "source": "aws.s3",
  "detail-type": "Object Created",
  "resources": ["arn:aws:s3:::my-bucket"],
  "detail": {
    "version": "0",
    "bucket": { "name": "my-bucket" },
    "object": { "key": "docs/hello.txt", "size": 11, "etag": "…" },
    "reason": "PutObject"
  }
}

detail-type is Object Created or Object Deleted; reason is the API operation (PutObject, CopyObject, CompleteMultipartUpload, DeleteObject); a delete carries "deletion-type", which is "Permanently Deleted" for a real removal and "Delete Marker Created" when a versioned bucket wrote a tombstone instead.

object.version-id is present for a bucket with version history, and object.sequencer is present on every object event — the hex string AWS documents consumers to compare in order to sequence two events for the same key. The same two values appear as versionId and sequencer in the Records[].s3.object payload delivered to SQS and Lambda.

The detail is still intentionally partial: request-id, requester and source-ip-address are omitted rather than invented, because Overcast has no truthful value for them and a fabricated request ID would look real to a consumer correlating events. The other detail-type values AWS emits — restore, storage-class, tagging and ACL events — have no corresponding operation in Overcast yet and so are never published.

CloudFormation’s AWS::S3::Bucket.NotificationConfiguration.EventBridgeConfiguration dispatches through PutBucketNotificationConfiguration. CloudFormation spells it as an EventBridgeEnabled flag whose only legal value is true; an explicit false is refused, because the S3 API has no spelling for it other than the element’s absence.

Summary

Category✅ Supported⚠️ Partial❌ Unsupported
Buckets8
CORS12
Website12
Objects113
Multipart uploads61
ACLs & policies34
Versioning3
Tagging3
Lifecycle21
Notifications2

Endpoints

Buckets

OperationStatusNotesAWS Docs
CreateBucket✅ SupportedAccount regional namespaces via x-amz-bucket-namespace: account-regionaldocs
DeleteBucket✅ SupportedBucket must be emptydocs
HeadBucket✅ Supporteddocs
ListBuckets✅ Supporteddocs
GetBucketLocation✅ Supporteddocs
GetBucketEncryption✅ SupportedReturns default SSE-S3 config; stores AES256/KMS bucket encryption rulesdocs
PutBucketEncryption✅ SupportedStores AES256/KMS bucket encryption rulesdocs
DeleteBucketEncryption✅ Supporteddocs

CORS

OperationStatusNotesAWS Docs
GetBucketCors⚠️ PartialCORS rules; rule Id is not yet preserveddocs
PutBucketCors⚠️ PartialCORS rules; rule Id is not yet preserveddocs
DeleteBucketCors✅ Supporteddocs

Website

OperationStatusNotesAWS Docs
GetBucketWebsite⚠️ PartialReturns the whole configuration — IndexDocument, ErrorDocument, RedirectAllRequestsTo and RoutingRules; Overcast serves no website endpoint, so nothing is actually redirecteddocs
PutBucketWebsite⚠️ PartialStores IndexDocument, ErrorDocument, RedirectAllRequestsTo and RoutingRules with AWS’s mutual exclusion and Protocol enum enforced; HttpRedirectCode values are not validated, and Overcast serves no website endpointdocs
DeleteBucketWebsite✅ Supporteddocs

Objects

OperationStatusNotesAWS Docs
PutObject✅ SupportedStores body + x-amz-meta-* headersdocs
GetObject✅ SupportedReturns body, ETag, metadata headers; versionId selects a specific version, and a delete-marked key answers 404 with x-amz-delete-markerdocs
HeadObject✅ Supporteddocs
DeleteObject✅ SupportedIdempotent — 204 for missing keys; in a versioned bucket a delete adds a delete marker, and versionId removes one version permanentlydocs
CopyObject✅ Supportedx-amz-copy-source may name a source versionIddocs
ListObjectsV2✅ SupportedSupports prefix, delimiter, max-keys, start-after, and continuation-token paginationdocs
DeleteObjects✅ SupportedBatch delete up to 1000 keys; quiet mode supported; per-entry VersionId, DeleteMarker and DeleteMarkerVersionId reporteddocs
ListObjects✅ SupportedMarker-based pagination; supports prefix, delimiterdocs
GetObjectAttributes❌ Unsupportedstub; returns 501docs
PutObjectTagging✅ Supporteddocs
GetObjectTagging✅ Supporteddocs
DeleteObjectTagging✅ Supporteddocs
RestoreObject❌ UnsupportedGlacier restore simulationdocs
SelectObjectContent❌ UnsupportedS3 Select (SQL queries on objects)docs

Multipart uploads

OperationStatusNotesAWS Docs
CreateMultipartUpload✅ Supporteddocs
UploadPart✅ Supporteddocs
UploadPartCopy❌ Unsupportedstub; returns 501docs
CompleteMultipartUpload✅ Supporteddocs
AbortMultipartUpload✅ Supporteddocs
ListMultipartUploads✅ Supporteddocs
ListParts✅ Supporteddocs

ACLs & policies

OperationStatusNotesAWS Docs
GetBucketAcl❌ Unsupportedstub; returns 501docs
PutBucketAcl❌ Unsupportedstub; returns 501docs
GetObjectAcl❌ Unsupportedstub; returns 501docs
PutObjectAcl❌ Unsupportedstub; returns 501docs
GetBucketPolicy✅ Supporteddocs
PutBucketPolicy✅ Supporteddocs
DeleteBucketPolicy✅ Supporteddocs

Versioning

OperationStatusNotesAWS Docs
GetBucketVersioning✅ Supporteddocs
PutBucketVersioning✅ SupportedEnabled and Suspended, with AWS’s semantics for both; objects that predate the change become their key’s null versiondocs
ListObjectVersions✅ SupportedVersions and delete markers in AWS’s order (key ascending, then most recent first), with prefix, delimiter, max-keys and key-marker/version-id-marker paginationdocs

Tagging

OperationStatusNotesAWS Docs
GetBucketTagging✅ Supporteddocs
PutBucketTagging✅ Supporteddocs
DeleteBucketTagging✅ Supporteddocs

Lifecycle

OperationStatusNotesAWS Docs
GetBucketLifecycleConfiguration✅ SupportedNoSuchLifecycleConfiguration when none is set; reports x-amz-transition-default-minimum-object-sizedocs
PutBucketLifecycleConfiguration⚠️ PartialExpiration, Transition, NoncurrentVersionExpiration, NoncurrentVersionTransition, ExpiredObjectDeleteMarker, AbortIncompleteMultipartUpload and prefix/tag/size filters are applied by an hourly sweeper; x-amz-transition-default-minimum-object-size gates transitions of objects under 128 KB, noncurrent ones included; expiring the current version of a versioned object adds a delete marker rather than deleting itdocs
DeleteBucketLifecycle✅ Supporteddocs

Notifications

OperationStatusNotesAWS Docs
GetBucketNotificationConfiguration✅ SupportedReturns empty config if none setdocs
PutBucketNotificationConfiguration✅ SupportedSQS, SNS, Lambda and EventBridge destinations; prefix/suffix filters. Records carry versionId and sequencer; SNS deliveries carry the Records JSON as the notification envelope’s Message string with Subject “Amazon S3 Notification”, as real S3 does; EventBridge events carry AWS’s Object Created/Object Deleted shape, including deletion-type, minus the fields Overcast has no value fordocs