Proposal
Promote the internal key-addressed path to the public web API:
// Caller names the S3 key; the Request contributes method (GET/HEAD) and
// conditional/Range headers. The request URL plays no role in key selection.
fetchWebKey(key: string, request: Request): Promise<Response>
fetchWeb then becomes a thin adapter over it:
fetchWeb(request) {
return this.fetchWebKey(keyFromUrl(request), request);
}
No new behavior — this publishes a seam that already exists internally (fetchKey is private) and sharpens the v4 layering: URL→key mapping is adapter business; the core primitive is "stream this key with these request semantics."
Motivation
Today the public web API derives the key exclusively from the request URL path. Any consumer whose URL→key mapping is not identity has to fabricate a synthetic Request to smuggle the key through the URL:
// e.g. an app mounting a bucket under /files/*
const key = url.pathname.replace(/^\/files\//, ''); // derive key
url.pathname = `/${key}`; // re-embed in a URL
return proxy.fetchWeb(new Request(url, c.req.raw)); // s3proxy re-parses key from URL
The key round-trips key → URL-encode → Request → URL-parse → key. Three costs, ascending:
- Waste — a synthetic
URL + Request per request, and a duplicated parse.
- Split ownership — "how a path becomes a key" lives in two places that must agree.
- Encoding hazard (the real one) — the key is smuggled through URL encoding/decoding. For keys containing
%, +, spaces, or the specialCharacters!... fixture already in the test bucket, the consumer's encoder and s3proxy's decoder must agree exactly, or the wrong object is served / a valid key 404s. Identity mapping never hits this; every rewriting consumer does.
Consumer classes that need non-identity mapping:
- Sub-path mounts — serving a bucket under
/files/*, /assets/* inside an existing app
- Multi-tenant prefix injection — request identity resolves to a server-side key prefix (
tenants/<org>/…) the client never sees; a security property, since the client shouldn't dictate the raw key
- Key rewriting — default documents, versioned keys, vanity paths
Design notes
- Alternative shape: an options override on the existing method —
fetchWeb(request, { key }). Smaller surface; the separate method is arguably more honest (URL ignored vs. URL authoritative), maintainer's call.
- Contract to document:
key is the raw S3 key (not URL-encoded); method must be GET/HEAD; Range/If-* headers flow through unchanged.
- Conformance case:
fetchWebKey(weirdKey, req) must behave identically to a correctly-encoded fetchWeb for the special-characters fixture.
Estimated scope: ~10 lines plus tests. The cost is permanent public API surface on a deliberately minimal package — hence framing as promoting the existing internal seam, not adding a feature.
Proposal
Promote the internal key-addressed path to the public web API:
fetchWebthen becomes a thin adapter over it:No new behavior — this publishes a seam that already exists internally (
fetchKeyis private) and sharpens the v4 layering: URL→key mapping is adapter business; the core primitive is "stream this key with these request semantics."Motivation
Today the public web API derives the key exclusively from the request URL path. Any consumer whose URL→key mapping is not identity has to fabricate a synthetic Request to smuggle the key through the URL:
The key round-trips key → URL-encode → Request → URL-parse → key. Three costs, ascending:
URL+Requestper request, and a duplicated parse.%,+, spaces, or thespecialCharacters!...fixture already in the test bucket, the consumer's encoder and s3proxy's decoder must agree exactly, or the wrong object is served / a valid key 404s. Identity mapping never hits this; every rewriting consumer does.Consumer classes that need non-identity mapping:
/files/*,/assets/*inside an existing apptenants/<org>/…) the client never sees; a security property, since the client shouldn't dictate the raw keyDesign notes
fetchWeb(request, { key }). Smaller surface; the separate method is arguably more honest (URL ignored vs. URL authoritative), maintainer's call.keyis the raw S3 key (not URL-encoded); method must be GET/HEAD; Range/If-* headers flow through unchanged.fetchWebKey(weirdKey, req)must behave identically to a correctly-encodedfetchWebfor the special-characters fixture.Estimated scope: ~10 lines plus tests. The cost is permanent public API surface on a deliberately minimal package — hence framing as promoting the existing internal seam, not adding a feature.