I recently had to work with an API that serves images protected by header-based
authentication. You have to send a bearer token in the
Authorization
header for all requests, including
images. How do you display such an image in a web page?
<img src='https://api.example.com/secret-image.png' />
This won’t work because an <img>
tag cannot send a custom header. You’ll
probably get some flavor of 401 Unauthorized
error. It would be
trivial to make it work if authentication was based on an URL query parameter or
a cookie, but it’s not as easy with a header.
Fetch the image
You’ll have to make the HTTP call to get the image yourself so you can send that header. For example, you could do this using the Fetch API:
function fetchWithAuthentication(url, authToken) {
const headers = new Headers();
headers.set('Authorization', `Bearer ${authToken}`);
return fetch(url, { headers });
}
The fetch response contains binary data, for example PNG data. You need to insert this data into the DOM somehow.
Embed the image with a Base64 data URL
My first attempt was to embed the image using a data URL. I used this function I found on Stack Overflow to perform the binary-to-Base64 conversion:
function arrayBufferToBase64(buffer: ArrayBuffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
You can use the arrayBuffer
method of the fetch
Response
to get the array buffer to pass to the Base64
conversion function, then build a properly formatted data URL and use it as the
source of the image:
async function displayProtectedImage(
imageId, imageUrl, authToken
) {
// Fetch the image.
const response = await fetchWithAuthentication(
imageUrl, authToken
);
// Convert the data to Base64 and build a data URL.
const binaryData = await response.arrayBuffer();
const base64 = arrayBufferToBase64(binaryData);
const dataUrl = `data:image/png;base64,${base64}`;
// Update the source of the image.
const imageElement = getElementById(imageId);
imageElement.src = dataUrl;
}
Here’s how to use it:
const imageId = 'some-image';
const imageUrl = 'https://api.example.com/secret-image.png';
const authToken = 'changeme';
displayProtectedImage(imageId, imageUrl, authToken);
Yay!
So slow…
It works but I ran into an issue: if you try to display several images like this at the same time and they are large enough, converting all this binary data to Base64 will slow down your UI.
This is because JavaScript in the browser runs on a single-threaded event loop. Since your JavaScript runs in the same thread as the UI, any sufficiently heavy calculation will temporary block everything, making your site feel unresponsive. (I recommend Philip Roberts’ excellent video What the heck is an event loop anyway? if you want to learn more about this topic.)
Maybe a web worker could be a solution to make sure this work is done in a separate thread, but I did not try going down that path.
Use an object URL
In the end, I used the URL Web API, specifically its
createObjectURL
function. If you have a blob of
binary data, you can pass it to this function to create a blob:
URL that
points to this data in memory:
URL.createObjectURL(blob);
// blob:http://example.com/e88f2e72-94c6-4f79-a40d-fc6749ce
If your blob contains image data, you can use this new URL as the source of an
<img>
tag. The advantage of this technique compared to constructing a data URL
is that you do not have to process the image data at all. This blob URL is
simply a pointer to the existing data in memory, with no extra computation
required. The data is stored in the blob URL store, a feature
of the File API.
You can get the data of a fetch Response
as a blob by using its blob
function. Let’s rewrite the displayProtectedImage
function to
take advantage of object URLs:
async function displayProtectedImage(
imageId, imageUrl, authToken
) {
// Fetch the image.
const response = await fetchWithAuthentication(
imageUrl, authToken
);
// Create an object URL from the data.
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
// Update the source of the image.
const imageElement = getElementById(imageId);
imageElement.src = objectUrl;
}
You can use it the same way as the previous version:
const imageId = 'some-image';
const imageUrl = 'https://api.example.com/secret-image.png';
const authToken = 'changeme';
displayProtectedImage(imageId, imageUrl, authToken);
Collect the garbage
The memory referenced by object URLs is released automatically when the document
is unloaded. However, if you’re writing a single page application with no page
refresh or if you generally care about memory consumption and performance, you
should let the browser know when this memory can be released by calling the
revokeObjectURL
function:
URL.revokeObjectURL(someObjectUrl);
In the case of an image, it’s easy to do it automatically as soon as the image
is done loading by using its onload
callback. Once the image is displayed, the
object URL and the referenced data are no longer needed:
imageElement.src = objectUrl;
imageElement.onload = () => URL.revokeObjectUrl(objectUrl);
Here’s an updated displayProtectedImage
function that does this:
async function displayProtectedImage(
imageId, imageUrl, authToken
) {
// Fetch the image.
const response = await fetchWithAuthentication(
imageUrl, authToken
);
// Create an object URL from the data.
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
// Update the source of the image.
const imageElement = getElementById(imageId);
imageElement.src = objectUrl;
imageElement.onload = () => URL.revokeObjectUrl(objectUrl);
}
May your UI remain swift.