Skip to content

M8-02 CDN fetch with host allow list

No Date Ruling Reason
D-M8-02-0 2026-10-15 The ticket was built on an integration base and ported onto main with only its own commits; the rows stand in a section per ticket like the other ported tickets. Brief, spec and REUSE were read from main; the brief relies on no core rule and no contract change, and chapter 22 already describes the behaviour (6.5, 14, 16), so no spec text changes. Additions to the names of the brief (D-W-100): CDNConfig also carries MaxRetries, MaxRateWait (the 429 limits of requirement 16), UserAgentVersion and Egress (as in Config of M8-01, so that only a test rule with allow_private opens the loopback fake); AttachmentRef.URL is a SignedURL, so that a printed reference never shows the signature; the one request construction of the package moved into newGet, which the API client and the fetcher share, because TestClientHasOnlyGet allows exactly one; the fetcher also returns *CallError (op GET <scheme://host/path>) and the 429 sentinels of the client. The tickets are built in parallel on integration bases and main takes each ticket as one pull request (precedent D-M8-01-0); the brief allows following the code where names differ, and each addition keeps a rule of the brief provable.
D-M8-02-1 2026-10-15 One platform/egress client per fetcher with a host policy of exactly the entries of import.discord.cdn_hosts over https, RedirectNever, Timeout = import.http_timeout and MaxResponseBytes = import.attachment_max_bytes plus one, instead of one transport per allowed host (REUSE 5.2). The fetcher checks every link against the same list before the request, so egress.allowed_hosts adds no CDN host. The per-request host policy gives the same guarantee: no connection is shared across a host the policy did not allow, because the pool keys connections by scheme, host and port.
D-M8-02-2 2026-10-15 Re-read rule: on 403 or 404 the body is closed unread, MessagesAround(channel, message, 1) reads the message once, the attachment with the same ID (also in a forwarded message snapshot) gives the fresh link, which passes the URL check and the declared size check again, and is fetched once. A missing message or attachment, or a second 403 or 404, is ErrCDNGone; 5xx and other statuses are a *CallError without retry. A 429 waits like the read client (retry_after, else Retry-After; a missing value counts as import.discord.max_rate_wait, a longer wait ends with ErrRateWaitTooLong); its body is read only for that value. 22 6.5 item 3 asks for exactly one re-read and one more try; the caller writes the jump-link line for a failed attachment (22 6.5 item 6, M8-04).
D-M8-02-3 2026-10-15 Port rule: a link matches an entry when its lower-cased host without one trailing dot equals the entry and its port equals the entry's port, 443 when the entry names none; an explicit :443 matches an entry without a port. Entries are DNS names with an optional :port, never IP literals or wildcards. Production entries name no port; tests write the fake's port into the entry as host:port (Design note 1).