Web developers have historically sworn allegiance to two layers of the web stack - frontend or backend. Some even choose both layers, and call themselves fullstack developers.
In a typical web application, your backend makes logical decisions on your server. Your frontend makes logical decisions on your users' devices. You want to deliver your static assets as quickly as possible to your users' devices, so they can load up your frontend faster. You probably accomplish this by putting your JavaScript, CSS, images, and video on a CDN, which stores the assets on servers that are closer to your users.
Your typical CDN offers some nice features. Static files are served from many servers around the world, so users don't have to wait as long for them. The edge servers can cache the resources, so they can be delivered to your users even more quickly. If you're lucky, you can even define unique rules for requests that match a regular expression.
But we think your typical CDN is too simple, so we made the edge programmable.
Moving backend logic to the edge
Suppose we have an application that serves stock photos. The photos are high quality and high resolution, and everyone seems to want them. So much so that we want to start restricting downloads of them, based on Referer (the Referer
[sic] header).
Referer isn't the safest way to restrict content, as it is pretty easy to fake. For our purposes though, it will get the point across to users: we want you to come to our site to get our content. If you get it any other way, you're going to get a poor version of it.
For requests that contain a Referer that we recognize, we'll respond with the high quality/high resolution original. If we get a Referer we don't recognize, or no Referer, we'll respond with a watermarked version.
The Old Way
Without Fly, we'd need to make a decision about which image gets served from our backend server.
Suppose a user wanted to look at an image of a tiny kitten. If a request made it to our backend server and it had a Referer we recognize, we'd render an image named tiny-kitten-full.jpg
.
If a request came into the backend server with a Referer we don't recognize, we'd render an image named tiny-kitten-watermarked.jpg
.
Our backend server would render multiple versions of the image, based on the Referer. Our CDN would be responsible for serving up those two images. This works....but it'd be nice if we could make the decision of which image to serve closer to the user.
The New Way
With Fly Edge Apps, we never need to check the Referer on the backend. The decision of which image to serve is moved closer to the user - at the edge.
Our backend server would render one image - named tiny-kitten.jpg
.
When our Fly Edge App serves up our tiny-kitten.jpg
image, it can check the Referer. If it recognizes the Referer, we'd serve up the original image. If not, we can add a watermark to the image, and return that instead.
Let's see what the code looks like for this app!
A Fly Edge App to restrict images by Referer
Our Fly Edge handler looks like this:
fly.http.respondWith(async function(request) {
const referer = request.headers.get('referer');
if (isValidReferer(referer)) {
return originalImage(request);
} else {
return watermarkedImage(request);
}
});
We grab the value of the referer
header from the request. If we find that it is valid, we return the original image. If the Referer is not valid, we return a watermarked image.
That's the general flow of our Fly Edge app. We're missing the implementations of several important functions, though. Let's fill them in!
Validating Referers
We'll use a very simple test to validate Referers. If the Referer is equal to our (completely made-up) URL, we'll consider it valid.
function isValidReferer(referer) {
return referer === 'https://totally-bogus-url.com';
}
You might want to do something more complex than this in real life. Maybe you have a regular expression you want to use, or a white-list of Referers.
Returning the original image
For cases when we want to return the original image, our function looks like this:
async function originalImage(request) {
const imagePath = extractImagePath(request);
const body = await loadImageBuffer(`file://assets${imagePath}`);
return new Response(body, {
headers: {
'Content-Type': 'image/jpg',
},
});
}
We'll extract the path of the requested image, load the original image, then return it as a Response
object, with Content-Type
of image/jpg
.
Extracting the requested image path
The first thing we'll need to do is identify the path of the image being requested. We do this in extractImagePath
:
function extractImagePath(request) {
const url = new URL(request.url);
return url.pathname;
}
We're using the URL
object to extract a pathname
property from the request URL.
Loading the original image
In the loadImageBuffer
function, we'll use the fetch
API to retrieve the original, and return an ArrayBuffer
:
async function loadImageBuffer(path) {
const response = await fetch(path);
if (response.status != 200) {
throw new Error("Couldn't load image: " + path);
}
return await response.arrayBuffer();
}
If the fetch fails, and response.status
is not 200, we throw an Error
. When this happens, Fly will return a status of 500 to the original request.
Where do the original images come from?
In this example, we're storing the original as a file on our file system. You can tell by the path of the file, which begins with file://
.
const body = await loadImageBuffer(`file://assets${imagePath}`)
Check out our docs to learn more about how files work.
In a more complex app, you might want to retrieve your images from an Amazon S3 bucket, or some other place your assets are stored.
Returning a watermarked version
Requests from unknown Referers will fall into the watermarked branch of our app. The code for this branch looks like this:
async function watermarkedImage(request) {
const imagePath = extractImagePath(request);
const [original, logo] = await Promise.all([
loadFlyImage(`file://assets${imagePath}`),
loadFlyImage(`file://assets/logo.png`),
]);
const body = await applyWatermark(original, logo);
return new Response(body.data, {
headers: {
'Content-Type': 'image/jpg',
},
});
}
We reuse our extractImagePath
function to identify the image being requested. We use Promise.all
to wait for two requests to complete concurrently: one that retrieves the originally requested image (using the extracted imagePath
), and one that retrieves our logo.
Given those assets, we'll apply a watermark to the original, then return the watermarked version as a Response
object, with Content-Type
of image/jpg
.
Loading Fly images
When we were returning the original image, we loaded images as a buffer. In order to watermark our image, we're going to want to load this buffer into a Fly Image
object. We do this in a loadFlyImage
function:
async function loadFlyImage(path) {
const buffer = await loadImageBuffer(path);
return new Image(buffer);
}
Applying the watermark
Our applyWatermark
function will take two Image
objects: the original, and a logo.
async function applyWatermark(original, logo) {
original.overlayWith(logo, { gravity: Image.gravity.center });
return await original.toBuffer();
}
We call overlayWith
on our original
Image object, and then return it as a buffer.
There are lots of cool things we can do with Image
objects. You can resize, crop, add opacity, convert formats...all sorts of things. You can read more about them here.
More possibilities
In this example, we're restricting by Referer, and returning a watermarked image. Here are some other things you could do to restrict your images:
- Rate-limit the amount of times a particular image is served.
- Return a smaller image (using the fly/image module).
- Restrict images for anonymous users, but serve the original to authenticated users (using an Authentication header in the request).
Conclusion
All of the things we did in this app to restrict image access could have been done on the server, but we were able to accomplish it all at the edge. This moves complexity closer to the users. They no longer have to wait for our backend servers to process their images. They get their images more quickly, and that makes them happy!
There are many other uses for Fly Edge apps. Check out our articles for more examples!