The s3:// source type: an object in a bucket.
s3://masters/2026/piece-final.wav is a bucket and a key, split at the first
/. Both halves are required — a source naming only a bucket, or only a key,
is refused rather than guessed at.
The key is kept as its raw decoded bytes. AudioProxy.Source has already
peeled off exactly one layer of encoding, and what is left is what S3 stores:
a key is an opaque byte string, a b.wav and a+b.wav are different objects,
and nothing here folds them together. Neither is a//b collapsed the way
local:// collapses it — an empty path segment is a perfectly ordinary
character in a key, and S3 will hand you back a different object for it.
Policy
The bucket is matched against AP_SOURCE_ALLOWLIST
(AudioProxy.Source.Allowlist), where an unset list accepts everything: the
proxy's S3 credentials already decide which buckets are readable, so an
allowlist is a second, narrower gate rather than the only one. Refusals are
{:error, :not_allowed} → 404, indistinguishable from a missing object.
Limits
A bucket may be 63 bytes and a key 1024 — S3's own maxima. Past them the object cannot exist, so refusing early costs nothing and keeps an unbounded string out of an argv element and a log line.
The whole body is bounded before it is split, not after. Finding the first
/ is a memchr-style scan, so it is cheap when a separator turns up early and
linear in the whole body when none ever does — measured at 0.016 ms for 1 MB
and 0.177 ms for 10 MB of separator-free input. That is four orders of
magnitude short of the cost that made AudioProxy.Source.Local's cap a
denial-of-service control, so this bound is a protocol bound like
AudioProxy.Source.Https's rather than a scheduler defence. It is enforced
first anyway: an input that cannot name an object should not be scanned at
all, and the ordering is one less thing to re-derive later.
The seam
stat/1 is one AudioProxy.S3.head/2: the object's size answers the render
path's 413 before a subprocess starts, and its ETag is what /info's
validator hashes. ffmpeg_input/1 is one AudioProxy.S3.presign_get/3,
handed to ffmpeg as a single argv element — the source bytes never cross the
BEAM, and ffmpeg issues its own Range requests, so -ss on a two-hour master
reads only the bytes it needs.
Nothing is presigned at stat/1 time. Both flows call the two callbacks
separately, and a presigned URL has an expiry: minting one the caller may
never use is a credential with a lifetime and no purpose.
Failures classify by cause
AudioProxy.S3's error type is five atoms and one {:http, status, _} whose
status is unbounded, and this module maps all of it explicitly, with no
catch-all — an unmapped shape should crash a test rather than pick a
plausible status in production.
The status ranges are the part worth reading twice. An earlier revision of
this covered 4xx and 5xx and called that total, which it is not: ex_aws
turns S3's "your bucket is in another region" into {:http, 301, _}, and a
request that hit it raised FunctionClauseError and answered a bare 500 —
the exact outcome the no-catch-all rule exists to prevent, arrived at by
leaving a hole instead of a default.
:not_found → :not_found 404, the blind row
:access_denied → :not_found 404, the blind row
{:http, 4xx, _} → :not_found 404, the blind row
{:http, 3xx, _} → :not_configured 500 — a wrong-region redirect
:not_configured → :not_configured 500
{:http, 5xx, _} → :upstream_unavailable 502
{:transport, _} → :upstream_unavailable 502A {:http, 1xx/2xx, _} still has no clause, deliberately: AudioProxy.S3
only builds those for the multipart write path, which this module never
reaches. A 3xx other than 301 does not arrive either — ex_aws's own case
has no branch for one and raises inside the dependency first, which is not
something a clause here can repair.
Folding :access_denied into the 404 is the deliberate part. A bucket
policy that denies HEAD is indistinguishable from a missing object to the
client, which is the property §5's blind 404 exists to protect; the operator
gets the truth from the log line, which names the S3 reason, rather than from
a response body that would double as an existence oracle. A 4xx that is
neither goes the same way: it means we asked wrongly for an object the client
named, and the client cannot tell that apart from the object not being there.
An outage does not go there. {:transport, _} and an upstream 5xx say
nothing about whether the object exists, so answering 404 would report a
deletion that did not happen and edge-cache it for ten seconds, suppressing
the retry that would have worked. They answer 502 (no-store).
:invalid_range is unreachable — it belongs to get_stream/3, which this
module never calls — and so has no clause. If it ever appeared it would
raise, which is this codebase's convention for "this should be impossible".
Summary
Functions
Maps one AudioProxy.S3.error/0 shape to this type's own reason.
A short human-readable sentence for one of this type's own reasons.
Every rejection reason this type can produce.
Types
Functions
@spec classify(AudioProxy.S3.error()) :: :not_found | :not_configured | :upstream_unavailable
Maps one AudioProxy.S3.error/0 shape to this type's own reason.
One clause per shape, no catch-all — see the moduledoc's table for what each answers and why. Public so a test can enumerate the error type exhaustively: the mapping is the whole of this slice's decision, and a shape that quietly lost its clause would answer 500 to a request rather than fail a test.
A short human-readable sentence for one of this type's own reasons.
@spec reasons() :: [atom()]
Every rejection reason this type can produce.
Declared rather than inferred, so a test can hold the two ends together:
AudioProxy.ErrorJSON has no catch-all, so a reason with no row there
raises FunctionClauseError in production. A new reason added here without
a row there fails that test instead of a request.
Not all of them are 404s. :not_configured and :upstream_unavailable come
from the storage seam and are deliberately not on
AudioProxy.ErrorJSON.not_found_reasons/0 — see the moduledoc's table for
why an outage must stay distinguishable from a missing object.