Skip to content

S3

Roto stores S3 bucket and object metadata in SQLite and object bodies as files. The AWS API, inspection UI, and browser view all read the same objects. Upload through an SDK, the AWS CLI, or Lua; the browser view immediately reflects the latest object versions. No export, separate site build, or website configuration is needed to browse a bucket.

Start and connect

Start a disposable server with populated example buckets:

scripts/demo.sh 5071

Open the inspection UI at http://localhost:5071/roto-api/, choose S3, then use Open in browser on a bucket or object. For a directory listing, try http://localhost:5071/s3/demo-assets/.

For an empty disposable server:

cargo run -p roto-server -- --ephemeral --port 5071

Use dummy credentials for local API requests. SDK clients should use the Roto endpoint and path-style S3 addressing:

import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:5071",
    region_name="us-east-1",
    aws_access_key_id="test",
    aws_secret_access_key="test",
    config=Config(s3={"addressing_style": "path"}),
)
s3.create_bucket(Bucket="my-bucket")
s3.put_object(
    Bucket="my-bucket",
    Key="foo.html",
    Body=b'<h1>Hello</h1><a href="../about.html">About</a>',
    ContentType="text/html; charset=utf-8",
)
s3.put_object(
    Bucket="my-bucket",
    Key="about.html",
    Body=b'<h1>About</h1><a href="../foo.html">Home</a>',
    ContentType="text/html; charset=utf-8",
)

Open http://localhost:5071/s3/my-bucket/foo.html. The HTML renders directly, and clicking About loads the other S3 object.

AWS API coverage

S3 uses the AWS REST-XML protocol. The main implemented API groups are:

Area Operations and behavior
Buckets Create, list, head, delete, and get location
Objects Put, get, head, copy, delete, batch delete, object attributes, metadata, range and conditional reads
Listings ListObjects and ListObjectsV2 with prefixes, delimiters, and pagination
Versioning Get/put versioning, list versions, explicit version reads and deletes, delete markers
Multipart Create upload, upload/copy parts, list uploads/parts, complete, abort
Tags and ACLs Bucket/object tagging and ACL read/write; ACLs are stored without permission enforcement
Configuration Website, CORS, lifecycle, encryption, replication, ownership controls, public access block, logging, notifications, acceleration, request payment, and policies

Configuration storage does not imply an execution engine. Lifecycle and replication settings are stored rather than running expiration or replication workers. Encryption configuration does not provide production cryptographic protection. Policies, public access settings, and ACLs are not a security boundary. Object lock, restore, select, detailed checksum behavior, and SNS/SQS notification delivery are outside the current core coverage. Unsupported modeled operations return NotImplemented rather than silently pretending to execute them.

The API also supports virtual-host addressing, for example my-bucket.s3.localhost:5071. This is an API hostname; use my-bucket.s3-website.localhost:5071 for browser index resolution and listings.

Versions and multipart uploads

Enable versioning through PutBucketVersioning. Replacing an object creates a new version, and deleting its current key creates a delete marker. Read or delete an older version by passing VersionId to the SDK operation. The inspection UI shows version IDs and latest/delete-marker flags; browser access uses the current version only.

Multipart uploads use the normal S3 sequence: create the upload, upload numbered parts, then complete with their part numbers and ETags. Incomplete uploads are visible through upload and part APIs and the inspection UI; they do not become browser objects until completed. Abort an upload to discard its pending parts.

Object notifications

Configure Lambda destinations using PutBucketNotificationConfiguration. Supported events include object creation through Put, Copy, and multipart completion, and removal through Delete/DeleteObjects, including delete markers. Prefix/suffix filters and event wildcards are applied. EventBridge integration also supports object events; see EventBridge.

Mutations persist notifications before asynchronous handoff. Persistent-mode queues survive restarts; delivery is at least once, so consumers should tolerate duplicates. Lambda handoff failures receive three attempts and remain visible at /roto-api/s3/notifications. Browser reads and directory listings do not trigger object mutation events. See Lambda S3 events for executor setup, delivery details, and inspection examples.

Three ways to read objects

Interface Example Behavior
AWS S3 API Signed GET /my-bucket/foo.html Normal S3 object response for SDKs and CLI clients
Browser GET /s3/my-bucket/foo.html Inline rendering, directory browsing, and index resolution
Inspection download /roto-api/s3/object?bucket=my-bucket&key=foo.html Attachment download; optional version selection

The browser paths do not replace the AWS API. Signed requests continue through normal API routing. Browser requests are unsigned, read-only GET or HEAD; other methods return 405. Write, copy, delete, list versions, and change bucket settings through the AWS API.

Browser reads select the latest object version. A latest delete marker hides the object. Use the inspection UI or S3 version APIs for older versions; a browser query string is not an S3 API operation selector or version selector.

Browser paths and directories

The basic URL is:

http://<server>:<port>/s3/<bucket>/<object-key>

It works with localhost, a DNS name, or an IP address. Object keys retain their S3 meaning: / separates apparent folders but does not create filesystem directories. Spaces, +, Unicode, ?, and # in keys must be URL encoded. Generated directory links encode those characters automatically.

Resolution follows these rules:

  1. A non-directory path first attempts to read the exact object key.
  2. A directory path attempts its index document, defaulting to index.html.
  3. If there is no index but the prefix contains objects or child prefixes, Roto displays a directory listing. Empty buckets also have a root listing.
  4. A directory requested without its trailing / redirects to the slash form, so relative links resolve correctly.
  5. A missing object or directory returns 404, using a configured error document when available. Missing buckets return 404 as well.

For example, /s3/my-bucket/docs/ renders docs/index.html if it exists; otherwise it lists immediate children under docs/. A stored exact object named docs takes precedence when requesting /s3/my-bucket/docs.

Listings contain parent-directory navigation and separate folder and object links. Large listings have a Next page link using S3 continuation tokens; they are not limited to the first page of objects. An index page takes precedence over listing its directory.

HTML, styles, scripts, and other content

HTML is rendered inline. CSS, JavaScript, images, and other assets are ordinary objects loaded by the browser. Roto preserves stored content types, content encoding, caching metadata, and ETags, and removes attachment disposition on browser reads. Range requests and ETag conditions are passed to S3 object reads. HEAD returns headers without a response body.

Set ContentType explicitly when uploading, especially for uncommon formats. For objects whose type is absent or generic (application/octet-stream or binary/octet-stream), the browser view infers common types from extensions: HTML, CSS, JavaScript, JSON, SVG, PNG, JPEG, GIF, WebP, ICO, text, PDF, WebAssembly, and WOFF/WOFF2. Other types keep their stored response type.

No HTML rewriting or JavaScript rewriting occurs. Links work according to normal browser URL resolution:

Link in /s3/my-bucket/docs/index.html Resolved URL
about.html /s3/my-bucket/docs/about.html
../assets/style.css /s3/my-bucket/assets/style.css
/s3/my-bucket/assets/style.css /s3/my-bucket/assets/style.css
/assets/style.css /assets/style.css, outside the bucket mount

For path-based browsing, use relative asset links or configure your site's build base as /s3/my-bucket/. This applies to JavaScript imports, fetch URLs, CSS url(...), forms, and client-side routers too. Roto does not automatically fallback every missing URL to an SPA index page.

Bucket website hostnames

For sites designed to run at an origin's root, use:

http://my-bucket.s3-website.localhost:5071/
http://my-bucket.s3-website.dev.example.com:5071/

Roto recognizes <bucket>.s3-website.<domain> in the HTTP Host header. The entire hostname serves that bucket, including paths such as /roto-api/health, which are object keys on a website host rather than inspection endpoints. /assets/ links therefore refer to objects under the bucket's assets/ prefix.

Many browsers resolve *.localhost to loopback. If your client does not, add local DNS or hosts-file entries. For remote use, configure DNS (for example, *.s3-website.dev.example.com) to point to the server. Bind the server to an accessible interface:

roto-server --host 0.0.0.0 --port 5071

An IP address alone works immediately with /s3/my-bucket/. It cannot select multiple buckets by hostname; choose path access, DNS names, or a reverse proxy that assigns a bucket hostname. A reverse proxy can also terminate HTTPS; retain the website Host header when forwarding requests to Roto. Generated local links and directory redirects use paths and work behind HTTPS proxies.

Website configuration and redirects

Bucket website configuration remains available through PutBucketWebsite, GetBucketWebsite, and DeleteBucketWebsite. It applies to both browser paths and website hostnames. For example:

s3.put_bucket_website(
    Bucket="my-bucket",
    WebsiteConfiguration={
        "IndexDocument": {"Suffix": "home.html"},
        "ErrorDocument": {"Key": "error.html"},
    },
)

The configured index suffix replaces index.html in each directory. Missing objects use the configured error object with HTTP status 404. If the error object is unavailable, Roto returns a basic 404 page. Deleting website configuration restores the default index and automatic listings; it does not disable browsing.

RedirectAllRequestsTo redirects to the configured host and optional protocol. Website routing rules match key prefixes and/or the resulting HTTP error status, then apply key replacement, host/protocol overrides, and a redirect status code. Rules are evaluated in order; the first match wins. These browser conveniences are not a complete reproduction of all AWS website endpoint edge cases.

Objects uploaded with WebsiteRedirectLocation redirect instead of displaying content. In path mode, a local destination such as /about.html is mounted under /s3/my-bucket/; absolute external URLs are preserved. Ordinary links inside HTML are not adjusted this way. Object and whole-bucket redirects default to 302; routing-rule redirects default to 301 unless configured otherwise.

Inspection, storage, and access boundaries

The inspection UI shows buckets, object versions, bucket configuration, and multipart resources. Open in browser opens the current object or bucket; Download retains attachment behavior, and Preview as text reads a bounded text preview. Opening an old version's row in the browser still selects the current version; downloading the row selects its explicit version.

Roto is a development simulator, not an authenticated static hosting service. Browser access does not enforce production S3 IAM or public-access policies. Path-based HTML shares the server's origin with the inspection UI, so only open content you trust. Website hostnames provide a separate browser origin from the inspection UI. Inspection downloads continue to force attachments rather than executing HTML on their own.

Persistent servers retain bucket metadata in the S3 database and bodies in the S3 blob store. Ephemeral servers discard state on exit. Browser reads never create objects or folders. See Storage and Services for the persistence model and broader S3 API support and limitations.

Verification

Build and run the browser integration checks using only Python's standard library; no Moto or Python packages are needed:

cargo build -p roto-server --locked
python3 tests/smoke-s3-browser.py
python3 tests/smoke-demo.py

The regular Rust CI job runs the browser integration check on every push and pull request, in addition to the Cargo tests. It can also be run locally with the commands above.

The browser check starts and stops its own fresh ephemeral server using the reusable demo seed. It verifies rendered HTML and asset types, directory/index resolution, encoded keys, website hosts, error documents, redirects, conditional and range reads, inspection downloads, API separation, and deleted objects.