Skip to main content
Ben Nadel at LaunchDarkly Lunch & Learn 2018 (New York, NY) with: Chelsie-Jean Fernandez
Ben Nadel at LaunchDarkly Lunch & Learn 2018 (New York, NY) with: Chelsie-Jean Fernandez@haulani7 )

Proxying Gravatar Images For Better Avatar Caching In ColdFusion

By on
Tags:

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.

UPDATE: Nov 9, 2022 - URL Rewriting For Dynamic .jpg Images

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

159 Comments

I did not know this was possible...eye opening 🤓. Now I'm wondering where I can use this technique 🤔

15,329 Comments

@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 the fileReadBinary() to pull down the image instead of using CFHttp (since ColdFusion can treat HTTP like a pseudo-file-system). But, I like the explicit logic in the CFContent tag.

15,329 Comments

@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).

15,329 Comments

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.

Post A Comment — I'd Love To Hear From You!

Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.