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
Hostheader for any of:bucket.s3.<anything>(includingbucket.s3.<region>.<anything>and the legacy dash dialectbucket.s3-<region>.<anything>), and the barebucket.<base>form forlocalhost, the built-in wildcard-DNS domains (localhost.overcast.sh,localhost.localstack.cloud,localhost.floci.io) and a configuredOVERCAST_HOSTNAME.localhost.overcast.shis 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
SecretAccessKeystored at key creation time. WhenOVERCAST_SIGV4_VALIDATEis enabled, invalid or expired presigned URLs return403 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, andListObjectVersionsreports each object as its key’snullversion — 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 noversionIdremoves nothing: it adds a delete marker that becomes the current version. A delete that names aversionIdremoves 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
nullversion, replacing any previous null version; and a delete still leaves a delete marker behind, with version idnull. 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:
| Request | Answer |
|---|---|
GET/HEAD of a key whose current version is a delete marker | 404 NoSuchKey with x-amz-delete-marker: true |
GET/HEAD naming a delete marker’s own versionId | 405 MethodNotAllowed with x-amz-delete-marker: true and Allow: DELETE |
GET/HEAD naming a versionId that does not exist | 404 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’
ListObjectVersions → DeleteObjects 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:
Expirationon the current version of a versioned object adds a delete marker rather than deleting anything. On an unversioned bucket it still deletes outright.NoncurrentVersionExpirationpermanently removes versions that have been noncurrent forNoncurrentDays. A version’s noncurrent clock starts when the version that replaced it was written, not when the version itself was written.NewerNoncurrentVersionsretains that many newer noncurrent versions before the action applies to the rest.NoncurrentVersionTransitionmarks 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.ExpiredObjectDeleteMarkerremoves 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 alongsideDaysorDatein the sameExpiration, 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 toGLACIER(Glacier Flexible Retrieval) orDEEP_ARCHIVE(Glacier Deep Archive); every other class keeps the 128 KB floor.- A rule whose own
FiltersetsObjectSizeGreaterThanorObjectSizeLessThanopts 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:
RedirectAllRequestsTowith itsHostNameand optionalProtocol.IndexDocumentandErrorDocument.RoutingRules, each with an optionalCondition(HttpErrorCodeReturnedEquals,KeyPrefixEquals) and aRedirect(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:
HttpRedirectCodevalues 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 |
|---|---|---|---|
| Buckets | 8 | ||
| CORS | 1 | 2 | |
| Website | 1 | 2 | |
| Objects | 11 | 3 | |
| Multipart uploads | 6 | 1 | |
| ACLs & policies | 3 | 4 | |
| Versioning | 3 | ||
| Tagging | 3 | ||
| Lifecycle | 2 | 1 | |
| Notifications | 2 |
Endpoints
Buckets
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
CreateBucket | ✅ Supported | Account regional namespaces via x-amz-bucket-namespace: account-regional | docs |
DeleteBucket | ✅ Supported | Bucket must be empty | docs |
HeadBucket | ✅ Supported | docs | |
ListBuckets | ✅ Supported | docs | |
GetBucketLocation | ✅ Supported | docs | |
GetBucketEncryption | ✅ Supported | Returns default SSE-S3 config; stores AES256/KMS bucket encryption rules | docs |
PutBucketEncryption | ✅ Supported | Stores AES256/KMS bucket encryption rules | docs |
DeleteBucketEncryption | ✅ Supported | docs |
CORS
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketCors | ⚠️ Partial | CORS rules; rule Id is not yet preserved | docs |
PutBucketCors | ⚠️ Partial | CORS rules; rule Id is not yet preserved | docs |
DeleteBucketCors | ✅ Supported | docs |
Website
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketWebsite | ⚠️ Partial | Returns the whole configuration — IndexDocument, ErrorDocument, RedirectAllRequestsTo and RoutingRules; Overcast serves no website endpoint, so nothing is actually redirected | docs |
PutBucketWebsite | ⚠️ Partial | Stores IndexDocument, ErrorDocument, RedirectAllRequestsTo and RoutingRules with AWS’s mutual exclusion and Protocol enum enforced; HttpRedirectCode values are not validated, and Overcast serves no website endpoint | docs |
DeleteBucketWebsite | ✅ Supported | docs |
Objects
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
PutObject | ✅ Supported | Stores body + x-amz-meta-* headers | docs |
GetObject | ✅ Supported | Returns body, ETag, metadata headers; versionId selects a specific version, and a delete-marked key answers 404 with x-amz-delete-marker | docs |
HeadObject | ✅ Supported | docs | |
DeleteObject | ✅ Supported | Idempotent — 204 for missing keys; in a versioned bucket a delete adds a delete marker, and versionId removes one version permanently | docs |
CopyObject | ✅ Supported | x-amz-copy-source may name a source versionId | docs |
ListObjectsV2 | ✅ Supported | Supports prefix, delimiter, max-keys, start-after, and continuation-token pagination | docs |
DeleteObjects | ✅ Supported | Batch delete up to 1000 keys; quiet mode supported; per-entry VersionId, DeleteMarker and DeleteMarkerVersionId reported | docs |
ListObjects | ✅ Supported | Marker-based pagination; supports prefix, delimiter | docs |
GetObjectAttributes | ❌ Unsupported | stub; returns 501 | docs |
PutObjectTagging | ✅ Supported | docs | |
GetObjectTagging | ✅ Supported | docs | |
DeleteObjectTagging | ✅ Supported | docs | |
RestoreObject | ❌ Unsupported | Glacier restore simulation | docs |
SelectObjectContent | ❌ Unsupported | S3 Select (SQL queries on objects) | docs |
Multipart uploads
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
CreateMultipartUpload | ✅ Supported | docs | |
UploadPart | ✅ Supported | docs | |
UploadPartCopy | ❌ Unsupported | stub; returns 501 | docs |
CompleteMultipartUpload | ✅ Supported | docs | |
AbortMultipartUpload | ✅ Supported | docs | |
ListMultipartUploads | ✅ Supported | docs | |
ListParts | ✅ Supported | docs |
ACLs & policies
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketAcl | ❌ Unsupported | stub; returns 501 | docs |
PutBucketAcl | ❌ Unsupported | stub; returns 501 | docs |
GetObjectAcl | ❌ Unsupported | stub; returns 501 | docs |
PutObjectAcl | ❌ Unsupported | stub; returns 501 | docs |
GetBucketPolicy | ✅ Supported | docs | |
PutBucketPolicy | ✅ Supported | docs | |
DeleteBucketPolicy | ✅ Supported | docs |
Versioning
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketVersioning | ✅ Supported | docs | |
PutBucketVersioning | ✅ Supported | Enabled and Suspended, with AWS’s semantics for both; objects that predate the change become their key’s null version | docs |
ListObjectVersions | ✅ Supported | Versions and delete markers in AWS’s order (key ascending, then most recent first), with prefix, delimiter, max-keys and key-marker/version-id-marker pagination | docs |
Tagging
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketTagging | ✅ Supported | docs | |
PutBucketTagging | ✅ Supported | docs | |
DeleteBucketTagging | ✅ Supported | docs |
Lifecycle
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketLifecycleConfiguration | ✅ Supported | NoSuchLifecycleConfiguration when none is set; reports x-amz-transition-default-minimum-object-size | docs |
PutBucketLifecycleConfiguration | ⚠️ Partial | Expiration, 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 it | docs |
DeleteBucketLifecycle | ✅ Supported | docs |
Notifications
| Operation | Status | Notes | AWS Docs |
|---|---|---|---|
GetBucketNotificationConfiguration | ✅ Supported | Returns empty config if none set | docs |
PutBucketNotificationConfiguration | ✅ Supported | SQS, 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 for | docs |