From 969ac8dd7fdb8a255b54cd027f8e31540debb4d6 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 17:15:31 +0000 Subject: [PATCH] Let a re-render replace its own assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uploads refused to overwrite, so re-rendering a revision failed after a full encode with "The resource already exists". That made a RENDERER_VERSION bump unusable for its entire purpose: the version exists to say the renderer changed while the design did not, and applying it means writing new bytes to the same revision's path. The object path is owner/campaign/creative/revision, so nothing but this creative's own revision can be addressed by this write. An overwrite can only replace bytes this pipeline produced for this revision, which is the case we want to allow and the only one reachable. Objects stay immutable to the edge. Replacing them is a deliberate and rare act — a renderer change — and the alternative is a cache-busting query string on every ad URL forever. Co-Authored-By: Claude Opus 5 (1M context) --- lib/ads/video/storage.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/ads/video/storage.ts b/lib/ads/video/storage.ts index 6f3f3e2..aa0b7b5 100644 --- a/lib/ads/video/storage.ts +++ b/lib/ads/video/storage.ts @@ -89,9 +89,20 @@ export async function uploadRenderedAssets(args: { const key = objectKey(prefix, asset.profile, path.basename(filePath)); const { error } = await svc.storage.from(ASSET_BUCKET).upload(key, bytes, { contentType: contentTypeFor(filePath, asset.contentType), - upsert: false, - // A revision's objects never change, so the edge may keep them for a - // year. A new revision is a new path. + // A re-render of the same revision is replacing its own output, which + // is exactly what a RENDERER_VERSION bump asks for: the design did not + // change, the renderer did. Refusing the write made those re-renders + // impossible — every one failed with "The resource already exists" + // after burning a full encode. + // + // This is safe because the path is owner/campaign/creative/revision: + // nothing but this creative's own revision can be addressed here, so + // an overwrite can only ever replace bytes this pipeline produced for + // this revision. + upsert: true, + // A revision's objects are still treated as immutable by the edge. + // Replacing them is a deliberate, rare act — a renderer change — and + // the alternative is a cache-busting query string on every ad URL. cacheControl: "public, max-age=31536000, immutable", }); if (error) {