Proxying Gravatar Images For Better Avatar Caching In ColdFusion
When readers leave a comment on this blog, I render an avatar next to their authorship information. This avatar is served from Gravatar, which is (probably) the most popular avatar system on the web (brought to us by the same people who built WordPress). Unfortunately, serving avatars from Gravatar was hurting my Chrome LightHouse scores due to Gravatar's very short caching controls (5-mins). To help improve my LightHouse score, I'm starting to proxy the Gravatar images on my ColdFusion server, applying a custom Cache-Control
HTTP header.
This isn't my first attempt to use proxying in order to improve functionality. Earlier this year, I started proxying GitHub gist content in order to hot-swap my code blocks without having to override the native document.write()
method.
Of course, proxying isn't a flawless victory: what I gain in control, I lose in terms of complexity. And, not only does proxying add more moving parts to my blog, it also increases processing overhead and broadens the possible attack surface area for malicious actors.
That said, this is just a blog; so, I'm not too worried about the downsides. And, I'll deal with them if they ever become a problem.
When it comes to proxying image content, there are several approaches that a ColdFusion application can use, each with different trade-offs. To keep things as simple as possible, all I'm going to do is proxy the HTTP request to Gravatar, and then return the image binary with an extended Cache-Control
max-age
value (number of seconds that the image can be cached locally in the browser before it is considered "stale").
Since each avatar is going to be associated with someone who posted a comment on this site, it means that I can generate an avatar URL using their member ID. This has the added benefit of hiding the MD5 email hash from the browser (which will keep my reader's email addresses more secure).
Here's my Adobe ColdFusion 2021 end-point for proxying Gravatar images though my server:
<cfscript>
// Param request parameters.
param name="request.attributes.memberID" type="numeric";
param name="request.attributes.v" type="numeric" default=1;
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
member = application.memberService.getMemberByID( val( request.attributes.memberID ) );
emailHash = hash( member.email ).lcase();
cfhttp(
result = "gravatarResponse",
method = "get",
url = "https://www.gravatar.com/avatar/#emailHash#",
getAsBinary = "yes",
timeout = 5
) {
// The size / dimensions of the avatar to return.
cfhttpparam(
type = "url",
name = "s",
value = "120"
);
// The d=404 tells Gravatar to return a 404 Not Found response if the given email
// does not have an associated avatar. This gives us the ability to provide our
// own, dynamic avatar (though, I'm not currently doing that yet).
cfhttpparam(
type = "url",
name = "d",
value = "404"
);
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
DAY_IN_SECONDS = ( 60 * 60 * 24 );
WEEK_IN_SECONDS = ( DAY_IN_SECONDS * 7 );
YEAR_IN_SECONDS = ( DAY_IN_SECONDS * 365 ); // Maximum TTL for caching.
// If the Gravatar exists, let's return it with an extended cache period (1-week).
if ( gravatarResponse.statusCode.reFind( "2\d\d") ) {
cfheader(
name = "Cache-Control",
value = "max-age=#WEEK_IN_SECONDS#, stale-while-revalidate=#YEAR_IN_SECONDS#"
);
cfcontent(
type = gravatarResponse.mimeType,
variable = gravatarResponse.fileContent
);
}
// If the Gravatar DOES NOT EXIST for the given member, let's return our fallback
// avatar with a shorter expiration date (1-day).
cfheader(
name = "Cache-Control",
value = "max-age=#DAY_IN_SECONDS#, stale-while-revalidate=#YEAR_IN_SECONDS#"
);
cfcontent(
type = "image/jpeg",
file = expandPath( "/images/gravatar/arnold.jpg" )
);
</cfscript>
As you can see, I'm using the CFHttp
tag to read-in the Gravatar image as a binary payload. The query-string parameter, d=404
, tells Gravatar to return a 404 Not Found
response if the avatar doesn't exist. This bifurcation of status codes allows me to serve up my own local image as a fallback. Right now, however, I'm continuing to use the Arnold Schwarzenegger image as the fallback avatar; but, I plan to do something more clever in the future.
If the Gravatar image exists, I'm setting a Cache-Control
max-age
of 1-week. However, I'm also passing in a v=1
query-string parameter to my ColdFusion page. In the future, I'm going to use the v
parameter to cache-bust the browser-cached avatar based on the user's commenting activity. But, for the moment, this v
value will just be hard-coded.
Now, if I open the Activity page for blog comments, we can see the local request for avatars being served up with a Cache-Control
header of 1-week:
Hopefully this should help improve my blog's LightHouse score; and, provide some improved cache performance for my readers.
I Tried to Cache Behind Cloudflare's CDN
As I mentioned above, proxying the Gravatar images adds extra load to my ColdFusion server. Initially, I was going to counterbalance this load by having Cloudflare cache the images in their CDN (which proxy's my ColdFusion blog). Unfortunately, it wasn't quite that simple. By default, Cloudflare only caches based on file extensions. Apparently, you can add Page Rules to have it cache dynamic content. However, from what I was reading, this only works for Business plan (and above) subscriptions. And, I'm currently on the Free plan.
In the future, I might try writing the avatars to disk, and then serving them up as actual image files via Cloudflare. But, that greatly increases the complexity of the solution; and, might very well be solving a problem that I don't have.
.jpg
Images
UPDATE: Nov 9, 2022 - URL Rewriting For Dynamic After posting this, a number of people suggested that I just use URL rewriting to serve up dynamic .jpg
images. At first, I didn't want to do this because I always want there to be less magic whenever possible. However, this morning, I sat down and looked at my existing URL rewriting rules, and I remember now that the rules essentially route any "file not found" I/O attempt through the ColdFusion application.
Meaning, if the browser makes a request for the non-existent image:
/does-not-exist/foo/bar.jpg
... the rewrite rules will see that this physical file doesn't exist and will rewrite it to be:
/index.cfm/does-not-exist/foo/bar.jpg
... where the original request is appended to the index.cfm
ColdFusion template as the cgi.path_info
value.
And, once I have the JPEG image URL coming in as the cgi.path_info
, then I just have to add an SEO (Search Engine Optimization) rule that maps that path-info onto the existing ColdFusion template that I have in the first part of this post:
<cfscript>
// Truncated SEO mapping patterns.
{
pattern = "^dynamic-images/avatars/([0-9]+)/([0-9]+)\.jpg",
attributes = "event=avatar&memberID=\1&v=\2"
},
</cfscript>
Now, I can define the avatar URLs using resource paths that end in .jpg
, which means that Cloudflare will now cache them.
Want to use code from this post? Check out the license.
Reader Comments
I did not know this was possible...eye opening 🤓. Now I'm wondering where I can use this technique 🤔
@Chris,
One thing that's really nice about the
CFContent
tag is that it accepts a binary value, so it makes it nice and easy to proxy. I mean, if you wanted to get even crazier, you could use thefileReadBinary()
to pull down the image instead of usingCFHttp
(since ColdFusion can treat HTTP like a pseudo-file-system). But, I like the explicit logic in theCFContent
tag.If you're using Nginx, you could also use it to proxy the requests:
https://woorkup.com/load-gravatars-from-cdn/
Doing it from the webserver would be my preferred method, since it already has mechanisms for doing the task and should be much more efficient at the task.
@Dan,
Great call-out and very clever approach! Unfortunately, I am not running nginx in production. I wouldn't be surprised if IIS has something similar; but, keeping it in the code just removes a little bit of my prod-vs-dev headache. It also gives me some wiggle room to change the default avatar (I'm considering generating one, but in the future).
I've update the post to include a section on URL rewriting. After some of you alls feedback, I decided to revisit my existing URL rewriting rules; and, it seems that I already had enough in place to make rewriting to a dynamic
.jpg
image happen. Check out the last section above.Now that I'm proxying the Gravatar images, I've been noodling on create per-user custom fallback avatars for people who don't have a Gravatar image:
www.bennadel.com/blog/4354-generating-fallback-avatars-using-cfimage-and-coldfusion.htm
This uses ColdFusion's image functionality to generate name-initials based avatars. Just a thought - not implemented yet.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →