Serve fallback image instead of 404 from r2

I'm using R2 to serve user avatars. If no avatar exists in the bucket for the given user ID, I'd like to return a default avatar as the 404 response. Is there any way to do this without using workers?
23 Replies
James
James13mo ago
I don’t believe there’s any way to do this outside of Workers
mrbaguvix
mrbaguvix13mo ago
As a side note, just curious, is there a reason you woudn't want to use workers for this scenerio?
James
James13mo ago
Costs, I would imagine
Matt
MattOP13mo ago
^^ We are doing this for user avatars, which get millions of loads. Also it just feels wrong to put compute in front of a bucket to change the server's 404 response ... I reluctantly put up a worker 😂
mrbaguvix
mrbaguvix13mo ago
Well, If i was to implement, I would use Workers KV in-betwee to avoid going to R2 each time to check.
Cyb3r-Jak3
Cyb3r-Jak313mo ago
Don’t know what your client code looks like but you could have it make a second request to the default user icon if the user specific one returns a 404
Erisa
Erisa13mo ago
KV is more expensive than R2 so thats not going to help
Chaika
Chaika13mo ago
Embed default avatar in Worker base64 encoded, use Cache (or a library like kotx/render to handle it for you) when getting the R2 Object/Image, if nothing return the default avatar. Wouldn't be too bad
Erisa
Erisa13mo ago
render even has the 404 behaviour as a native feature :) you can just do NOTFOUND_FILE = "404.png"
Chaika
Chaika13mo ago
oh does it? That's nice, would still be another R2 GET though I assume, whereas embedding it would be free (although a bit silly/limits you on size)
Erisa
Erisa13mo ago
Oh yeah for sure :P I also dont know if we cache 404s in render, I wrote the feature but I dont remember
Matt
MattOP12mo ago
No description
Matt
MattOP12mo ago
This is my very naive worker implementation for serving avatars with a fallback. Is there a material perf/cost penalty to just using fetch vs getting from the bucket directly? (FWIW both the bucket itself and the worker have aggressive cache rules)
Hello, I’m Allie!
Not sure I understand though, why aren’t you using an R2 Binding?
Matt
MattOP12mo ago
the simple answer is I haven't (yet) taken the time to understand them. That's probably the better call, but to get things going quickly I was basing this on https://www.mickaelvieira.com/blog/2020/01/27/custom-404-page-with-cloudflare-workers.html we have no other workers and this was just my quick and dirty attempt to get custom 404 responses
Mickaël Vieira
Custom 404 page with Cloudflare Workers - Mickaël Vieira - Software...
Hijacking 404 errors and returning a custom 404 page with Cloudflare Workers
Matt
MattOP12mo ago
That said I'm open to learning if you think it would be value add
Hello, I’m Allie!
One thing is that you can make it so that your R2 bucket isn't publicly available If that matters to you
Matt
MattOP12mo ago
Since these are public user avatars it's ok (intentional) to be publicly accessible, but mainly concerned about cost and perf. This worker will see millions of requests a day next month and want to brace myself 😂
Hello, I’m Allie!
No, I mean that, do you need the bucket to be publicly available without your Worker in front?
Matt
MattOP12mo ago
Not necessarily, but it's not a deal breaker either way (ty for looking by the way)
Hello, I’m Allie!
It could be something as simple as this:
import fallback from "./default.webp";

const cache = caches.default;

export default {
async fetch({ url }, { R2 }, ctx) {
let res = await cache.match(url);
if(res) {
return res;
}
const fromR2 = await R2.get(new URL(url).pathname);
if (!fromR2) {
return new Response(fallback, {
headers: {
"content-type": "image/webp"
}
});
}
res = new Response(fromR2.body, {
headers: {
"content-type": "image/webp"
"cache-control": "max-age=3600"
}
});
ctx.waitUntil(cache.put(url, res.clone()));
return res;
}
}
import fallback from "./default.webp";

const cache = caches.default;

export default {
async fetch({ url }, { R2 }, ctx) {
let res = await cache.match(url);
if(res) {
return res;
}
const fromR2 = await R2.get(new URL(url).pathname);
if (!fromR2) {
return new Response(fallback, {
headers: {
"content-type": "image/webp"
}
});
}
res = new Response(fromR2.body, {
headers: {
"content-type": "image/webp"
"cache-control": "max-age=3600"
}
});
ctx.waitUntil(cache.put(url, res.clone()));
return res;
}
}
You would just need to set up the bindings, and add a bundling rule for webp files Then you also don't get billed twice when a user avatar doesn't exist
Sam
Sam12mo ago
I don't know your back-end, but wouldn't it be a lot cheaper if you just store a boolean in your database to see if the user has an avatar or not. Also, if your user will get an avatar, the cache will probably still show the default avatar.
Hello, I’m Allie!
The code above specifically doesn't add cache headers to fallback images, if that's it
Want results from more Discord servers?
Add your server