Repository: withastro/astro
File: packages/astro/src/assets/build/remote.ts (TypeScript)

Code before:
		throw new Error(
			`Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`,
		);
	}

	// calculate an expiration date based on the response's TTL
	const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
	const expires = policy.storable() ? policy.timeToLive() : 0;

	return {
		data: Buffer.from(await res.arrayBuffer()),
		expires: Date.now() + expires,
		etag: res.headers.get('Etag'),
	};
}

/**
 * Revalidate a cached remote asset using its entity-tag.
 * Uses the [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) header to check with the remote server if the cached version of a remote asset is still up to date.
 * The remote server may respond that the cached asset is still up-to-date if the entity-tag matches (304 Not Modified), or respond with an updated asset (200 OK)
 * @param src - url to remote asset
 * @param etag - the stored Entity-Tag of the cached asset
 * @returns An ImageData object containing the asset data, a new expiry time, and the asset's etag. The data buffer will be empty if the asset was not modified.
 */
export async function revalidateRemoteImage(src: string, etag: string) {
	const req = new Request(src, { headers: { 'If-None-Match': etag } });
	const res = await fetch(req);

	// Asset not modified: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
	if (!res.ok && res.status !== 304) {
		throw new Error(
			`Failed to revalidate cached remote image ${src}. The request did not return a 200 OK / 304 NOT MODIFIED response. (received ${res.status} ${res.statusText})`,
		);
	}

	const data = Buffer.from(await res.arrayBuffer());

	if (res.ok && !data.length) {
		// Server did not include body but indicated cache was stale
		return await loadRemoteImage(src);
	}

	// calculate an expiration date based on the response's TTL
	const policy = new CachePolicy(
		webToCachePolicyRequest(req),
		webToCachePolicyResponse(
			res.ok ? res : new Response(null, { status: 200, headers: res.headers }),
		), // 304 responses themselves are not cachable, so just pretend to get the refreshed TTL
	);
	const expires = policy.storable() ? policy.timeToLive() : 0;


Review comment (suggestion):
It would be worth also supporting `If-Modified-Since`, and storing the `Last-Modified` or `Date` for image responses that don't include an etag.

Code after:
export async function loadRemoteImage(src: string) {
	const req = new Request(src);
	const res = await fetch(req);

	if (!res.ok) {
		throw new Error(
			`Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`,
		);
	}

	// calculate an expiration date based on the response's TTL
	const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
	const expires = policy.storable() ? policy.timeToLive() : 0;

	return {
		data: Buffer.from(await res.arrayBuffer()),
		expires: Date.now() + expires,
		etag: res.headers.get('Etag') ?? undefined,
		lastModified: res.headers.get('Last-Modified') ?? undefined,
	};
}

/**
 * Revalidate a cached remote asset using its entity-tag or modified date.
 * Uses the [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) and [If-Modified-Since](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since)
 * headers to check with the remote server if the cached version of a remote asset is still up to date.
 * The remote server may respond that the cached asset is still up-to-date if the entity-tag or modification time matches (304 Not Modified), or respond with an updated asset (200 OK)
 * @param src - url to remote asset
 * @param revalidationData - an object containing the stored Entity-Tag of the cached asset and/or the Last Modified time
 * @returns An ImageData object containing the asset data, a new expiry time, and the asset's etag. The data buffer will be empty if the asset was not modified.
 */
export async function revalidateRemoteImage(
	src: string,
	revalidationData: { etag?: string; lastModified?: string },
) {
	const headers = {
		...(revalidationData.etag && { 'If-None-Match': revalidationData.etag }),
		...(revalidationData.lastModified && { 'If-Modified-Since': revalidationData.lastModified }),
	};
	const req = new Request(src, { headers });
	const res = await fetch(req);

	// Asset not modified: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
	if (!res.ok && res.status !== 304) {
		throw new Error(
			`Failed to revalidate cached remote image ${src}. The request did not return a 200 OK / 304 NOT MODIFIED response. (received ${res.status} ${res.statusText})`,
		);
	}

	const data = Buffer.from(await res.arrayBuffer());