If you've ever spent hours debugging a production auth failure that works perfectly in local dev, this one's for you.
The Symptom
Our video player worked flawlessly locally but showed an infinite loading spinner in production. No error messages. No 4xx/5xx in the logs. Just... silence.
After checking every obvious thing (CORS, CSP, Clerk auth, Supabase RLS), we finally spotted the real culprit in the raw HTTP request headers.
The Root Cause: `
` in the Authorization Header
We'd set our Cloudflare R2 credentials on Vercel using a shell pipe like this:
echo "$R2_SECRET_ACCESS_KEY" | vercel env add R2_SECRET_ACCESS_KEY production
The problem? echo appends a trailing newline by default. Vercel stored mysecretkey — including the
— as the env var value.
When the AWS S3 SDK built the Authorization header, it included that newline character in the HMAC signature string. The result was an invalid HTTP header that R2 rejected silently (no meaningful error message from the S3 client — it just failed the request).
Why It's Hard to Spot
- Works locally because you set vars in
.env.localwithout trailing newlines - No obvious error message — the S3 client doesn't say "invalid auth header"
- The failure mode is silent: presigned URL generation appears to succeed, but the URL itself is malformed
The Fix
Always use echo -n (no trailing newline) when piping secrets:
echo -n "$R2_SECRET_ACCESS_KEY" | vercel env add R2_SECRET_ACCESS_KEY production
Or better yet, use the Vercel dashboard to paste values directly — it strips trailing whitespace.
After re-setting all three R2 vars (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT) with echo -n and force-redeploying, the video player started working immediately.
Bonus: The Second Bug We Found While Fixing the First
Once the auth was fixed, we noticed the video player was reload-looping. The <video src> was being reset on every status poll because our loadArtifacts function called setVideoUrl unconditionally on each invocation.
Fix: introduced a forceRefreshUrl flag — only update <video src> on first load or an explicit 6-hour refresh, not on routine polling.
const loadArtifacts = async (forceRefreshUrl = false) => {
const data = await fetchArtifacts(projectId);
if (forceRefreshUrl || !videoUrl) {
setVideoUrl(data.videoUrl);
}
// update other state...
};
Lessons
-
Always
echo -nwhen piping secrets via CLI — never trust the default -
Add a health endpoint that actively tests S3/R2 connectivity on startup (we now have
/api/healththat checks R2 on every deploy) - Decouple URL refresh from polling — your video src shouldn't change just because a background job polled for status
These kinds of bugs are invisible in dev, brutal in prod, and completely preventable once you know to look for them.
Top comments (0)