Proxying Gravatar Images For Better Avatar Caching In ColdFusion

Cyberdime
Published: November 7, 2022

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.

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.

Want to use code from this post?
Check out the license.

Source: www.bennadel.com