Skip to content

Commit 08b8c46

Browse files
authored
Add CF-Cache-Status to Workers Assets (cloudflare#8373)
* Move util final op methods to a more appropriate place * Add CF-Cache-Status header to Workers Assets
1 parent 4d9d9e6 commit 08b8c46

File tree

7 files changed

+225
-87
lines changed

7 files changed

+225
-87
lines changed

.changeset/thirty-emus-find.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cloudflare/workers-shared": minor
3+
---
4+
5+
Add `CF-Cache-Status` to Workers Assets responses to indicate if we returned a cached asset or not. This will also populate zone cache analytics and Logpush logs.

packages/workers-shared/asset-worker/src/analytics.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type Data = {
3636
version?: string;
3737
// blob7 - Region of the colo (e.g. WEUR)
3838
coloRegion?: string;
39+
// blob8 - The cache status of the request
40+
cacheStatus?: string;
3941
};
4042

4143
export class Analytics {
@@ -78,6 +80,7 @@ export class Analytics {
7880
this.data.error?.substring(0, 256), // blob5 - trim to 256 bytes
7981
this.data.version, // blob6
8082
this.data.coloRegion, // blob7
83+
this.data.cacheStatus, // blob8
8184
],
8285
});
8386
}

packages/workers-shared/asset-worker/src/handler.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { attachCustomHeaders, getAssetHeaders } from "./utils/headers";
1515
import { generateRulesMatcher, replacer } from "./utils/rules-engine";
1616
import type { AssetConfig } from "../../utils/types";
17+
import type { Analytics } from "./analytics";
1718
import type EntrypointType from "./index";
1819
import type { Env } from "./index";
1920

@@ -176,7 +177,8 @@ const resolveAssetIntentToResponse = async (
176177
assetIntent: AssetIntent,
177178
request: Request,
178179
env: Env,
179-
getByETag: typeof EntrypointType.prototype.unstable_getByETag
180+
getByETag: typeof EntrypointType.prototype.unstable_getByETag,
181+
analytics: Analytics
180182
) => {
181183
const { pathname } = new URL(request.url);
182184
const method = request.method.toUpperCase();
@@ -191,7 +193,13 @@ const resolveAssetIntentToResponse = async (
191193
return await getByETag(assetIntent.eTag);
192194
});
193195

194-
const headers = getAssetHeaders(assetIntent.eTag, asset.contentType, request);
196+
const headers = getAssetHeaders(
197+
assetIntent.eTag,
198+
asset.contentType,
199+
asset.cacheStatus,
200+
request
201+
);
202+
analytics.setData({ cacheStatus: asset.cacheStatus });
195203

196204
const strongETag = `"${assetIntent.eTag}"`;
197205
const weakETag = `W/${strongETag}`;
@@ -252,7 +260,8 @@ export const handleRequest = async (
252260
env: Env,
253261
configuration: Required<AssetConfig>,
254262
exists: typeof EntrypointType.prototype.unstable_exists,
255-
getByETag: typeof EntrypointType.prototype.unstable_getByETag
263+
getByETag: typeof EntrypointType.prototype.unstable_getByETag,
264+
analytics: Analytics
256265
) => {
257266
const responseOrAssetIntent = await getResponseOrAssetIntent(
258267
request,
@@ -268,7 +277,8 @@ export const handleRequest = async (
268277
responseOrAssetIntent,
269278
request,
270279
env,
271-
getByETag
280+
getByETag,
281+
analytics
272282
);
273283

274284
return attachCustomHeaders(request, response, configuration);

packages/workers-shared/asset-worker/src/index.ts

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { WorkerEntrypoint } from "cloudflare:workers";
22
import { PerformanceTimer } from "../../utils/performance";
3-
import { InternalServerErrorResponse } from "../../utils/responses";
43
import { setupSentry } from "../../utils/sentry";
54
import { mockJaegerBinding } from "../../utils/tracing";
65
import { Analytics } from "./analytics";
76
import { AssetsManifest } from "./assets-manifest";
87
import { applyConfigurationDefaults } from "./configuration";
98
import { ExperimentAnalytics } from "./experiment-analytics";
109
import { canFetch, handleRequest } from "./handler";
10+
import { handleError, submitMetrics } from "./utils/final-operations";
1111
import { getAssetWithMetadataFromKV } from "./utils/kv";
1212
import type {
1313
AssetConfig,
@@ -16,44 +16,6 @@ import type {
1616
UnsafePerformanceTimer,
1717
} from "../../utils/types";
1818
import type { Environment, ReadyAnalytics } from "./types";
19-
import type { Toucan } from "toucan-js";
20-
21-
function handleError(
22-
sentry: Toucan | undefined,
23-
analytics: Analytics,
24-
err: unknown
25-
) {
26-
try {
27-
const response = new InternalServerErrorResponse(err as Error);
28-
29-
// Log to Sentry if we can
30-
if (sentry) {
31-
sentry.captureException(err);
32-
}
33-
34-
if (err instanceof Error) {
35-
analytics.setData({ error: err.message });
36-
}
37-
38-
return response;
39-
} catch (e) {
40-
console.error("Error handling error", e);
41-
return new InternalServerErrorResponse(e as Error);
42-
}
43-
}
44-
45-
function submitMetrics(
46-
analytics: Analytics,
47-
performance: PerformanceTimer,
48-
startTimeMs: number
49-
) {
50-
try {
51-
analytics.setData({ requestTime: performance.now() - startTimeMs });
52-
analytics.write();
53-
} catch (e) {
54-
console.error("Error submitting metrics", e);
55-
}
56-
}
5719

5820
export type Env = {
5921
/*
@@ -157,7 +119,8 @@ export default class extends WorkerEntrypoint<Env> {
157119
this.env,
158120
config,
159121
this.unstable_exists.bind(this),
160-
this.unstable_getByETag.bind(this)
122+
this.unstable_getByETag.bind(this),
123+
analytics
161124
);
162125

163126
analytics.setData({ status: response.status });
@@ -171,7 +134,7 @@ export default class extends WorkerEntrypoint<Env> {
171134
}
172135
}
173136

174-
// TODO: Trace unstable methods
137+
// TODO: Add observability to these methods
175138
async unstable_canFetch(request: Request): Promise<boolean> {
176139
// TODO: Mock this with Miniflare
177140
this.env.JAEGER ??= mockJaegerBinding();
@@ -187,11 +150,16 @@ export default class extends WorkerEntrypoint<Env> {
187150
async unstable_getByETag(eTag: string): Promise<{
188151
readableStream: ReadableStream;
189152
contentType: string | undefined;
153+
cacheStatus: "HIT" | "MISS";
190154
}> {
155+
const performance = new PerformanceTimer(this.env.UNSAFE_PERFORMANCE);
156+
const startTime = performance.now();
191157
const asset = await getAssetWithMetadataFromKV(
192158
this.env.ASSETS_KV_NAMESPACE,
193159
eTag
194160
);
161+
const endTime = performance.now();
162+
const assetFetchTime = endTime - startTime;
195163

196164
if (!asset || !asset.value) {
197165
throw new Error(
@@ -202,12 +170,17 @@ export default class extends WorkerEntrypoint<Env> {
202170
return {
203171
readableStream: asset.value,
204172
contentType: asset.metadata?.contentType,
173+
// KV does not yet provide a way to check if a value was fetched from cache
174+
// so we assume that if the fetch time is less than 100ms, it was a cache hit.
175+
// This is a reasonable assumption given the data we have and how KV works.
176+
cacheStatus: assetFetchTime <= 100 ? "HIT" : "MISS",
205177
};
206178
}
207179

208180
async unstable_getByPathname(pathname: string): Promise<{
209181
readableStream: ReadableStream;
210182
contentType: string | undefined;
183+
cacheStatus: "HIT" | "MISS";
211184
} | null> {
212185
const eTag = await this.unstable_exists(pathname);
213186
if (!eTag) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { InternalServerErrorResponse } from "../../../utils/responses";
2+
import type { PerformanceTimer } from "../../../utils/performance";
3+
import type { Analytics } from "../analytics";
4+
import type { Toucan } from "toucan-js";
5+
6+
export function handleError(
7+
sentry: Toucan | undefined,
8+
analytics: Analytics,
9+
err: unknown
10+
) {
11+
try {
12+
const response = new InternalServerErrorResponse(err as Error);
13+
14+
// Log to Sentry if we can
15+
if (sentry) {
16+
sentry.captureException(err);
17+
}
18+
19+
if (err instanceof Error) {
20+
analytics.setData({ error: err.message });
21+
}
22+
23+
return response;
24+
} catch (e) {
25+
console.error("Error handling error", e);
26+
return new InternalServerErrorResponse(e as Error);
27+
}
28+
}
29+
30+
export function submitMetrics(
31+
analytics: Analytics,
32+
performance: PerformanceTimer,
33+
startTimeMs: number
34+
) {
35+
try {
36+
analytics.setData({ requestTime: performance.now() - startTimeMs });
37+
analytics.write();
38+
} catch (e) {
39+
console.error("Error submitting metrics", e);
40+
}
41+
}

packages/workers-shared/asset-worker/src/utils/headers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { AssetConfig } from "../../../utils/types";
1212
export function getAssetHeaders(
1313
eTag: string,
1414
contentType: string | undefined,
15+
cacheStatus: string,
1516
request: Request
1617
) {
1718
const headers = new Headers({
@@ -26,6 +27,10 @@ export function getAssetHeaders(
2627
headers.append("Cache-Control", CACHE_CONTROL_BROWSER);
2728
}
2829

30+
// Attach CF-Cache-Status, this will show to users that we are caching assets
31+
// and it will also populate the cache fields through the logging pipeline.
32+
headers.append("CF-Cache-Status", cacheStatus);
33+
2934
return headers;
3035
}
3136

0 commit comments

Comments
 (0)