Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/workerd/api/http.c++
Original file line number Diff line number Diff line change
Expand Up @@ -689,17 +689,19 @@ kj::Maybe<kj::String> Request::serializeCfBlobJson(jsg::Lock& js) {
if (!obj.has(js, "cacheControl")) {
if (hasCacheMode) {
// Synthesize from the cache request option.
switch (cacheMode) {
case CacheMode::NOSTORE:
obj.set(js, "cacheControl", js.str("no-store"_kjc));
break;
case CacheMode::NOCACHE:
obj.set(js, "cacheControl", js.str("no-cache"_kjc));
break;
case CacheMode::RELOAD:
break;
case CacheMode::NONE:
KJ_UNREACHABLE;
if (!FeatureFlags::get(js).getCacheOptionPreservesCacheControl()) {
switch (cacheMode) {
case CacheMode::NOSTORE:
obj.set(js, "cacheControl", js.str("no-store"_kjc));
break;
case CacheMode::NOCACHE:
obj.set(js, "cacheControl", js.str("no-cache"_kjc));
break;
case CacheMode::RELOAD:
break;
case CacheMode::NONE:
KJ_UNREACHABLE;
}
}
} else if (obj.has(js, "cacheTtl")) {
// Synthesize from cacheTtl value: positive/zero → max-age=N, -1 → no-store.
Expand Down
39 changes: 39 additions & 0 deletions src/workerd/api/tests/cf-cache-control-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

import assert from 'node:assert';

// Echoes the cf object this worker receives, so tests can inspect the cf blob a fetch serializes.
export default {
async fetch(request) {
return Response.json(request.cf ?? null);
},
};

// Tests for cf.cacheControl mutual exclusion and synthesis.
// These tests run regardless of cache option flag since cacheControl is always available.
export const cacheControlMutualExclusion = {
Expand Down Expand Up @@ -203,3 +210,35 @@ export const additionalCacheSettings = {
}
},
};

export const cacheOptionSerialization = {
async test(ctrl, env, ctx) {
if (!env.CACHE_ENABLED) return;
const expectSynthesized = !env.CACHE_OPTION_PRESERVES_CACHE_CONTROL;

const noCacheCf = await (
await env.SERVICE.fetch('http://placeholder/', { cache: 'no-cache' })
).json();
assert.strictEqual(noCacheCf.cacheForceRevalidate, true);
assert.strictEqual(
noCacheCf.cacheControl,
expectSynthesized ? 'no-cache' : undefined
);

const noStoreCf = await (
await env.SERVICE.fetch('http://placeholder/', { cache: 'no-store' })
).json();
assert.strictEqual(noStoreCf.cacheTtl, -1);
assert.strictEqual(noStoreCf.cacheLevel, 'bypass');
assert.strictEqual(
noStoreCf.cacheControl,
expectSynthesized ? 'no-store' : undefined
);

// cacheTtl still synthesizes cacheControl regardless of the flag.
const ttlCf = await (
await env.SERVICE.fetch('http://placeholder/', { cf: { cacheTtl: 300 } })
).json();
assert.strictEqual(ttlCf.cacheControl, 'max-age=300');
},
};
20 changes: 19 additions & 1 deletion src/workerd/api/tests/cf-cache-control-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,26 @@ const unitTests :Workerd.Config = (
],
bindings = [
( name = "CACHE_ENABLED", json = "true" ),
( name = "SERVICE", service = "cf-cache-control-test-cache-enabled" ),
( name = "CACHE_OPTION_PRESERVES_CACHE_CONTROL", json = "false" ),
],
compatibilityFlags = ["nodejs_compat", "cache_option_enabled"],
compatibilityFlags = ["nodejs_compat", "cache_option_enabled", "cache_no_cache_enabled",
"cache_option_overrides_cache_control"],
)
),
( name = "cf-cache-control-test-cache-option-preserves-cache-control",
worker = (
modules = [
( name = "worker-cache-option-preserves-cache-control",
esModule = embed "cf-cache-control-test.js" )
],
bindings = [
( name = "CACHE_ENABLED", json = "true" ),
( name = "SERVICE", service = "cf-cache-control-test-cache-option-preserves-cache-control" ),
( name = "CACHE_OPTION_PRESERVES_CACHE_CONTROL", json = "true" ),
],
compatibilityFlags = ["nodejs_compat", "cache_option_enabled", "cache_no_cache_enabled",
"cache_option_preserves_cache_control"],
)
),
],
Expand Down
12 changes: 12 additions & 0 deletions src/workerd/io/compatibility-date.capnp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Copyright (c) 2017-2022 Cloudflare, Inc.
# Licensed under the Apache 2.0 license found in the LICENSE file or at:
# https://opensource.org/licenses/Apache-2.0
Expand Down Expand Up @@ -1705,4 +1705,16 @@
# source fails, once that cancel completes. Previously, a destination that errored or closed
# while the pipe waited on a read canceled the source but resolved the pipe promise, or left it
# pending forever with preventCancel, and a failed cancel was not reported.

cacheOptionPreservesCacheControl @194 :Bool
$compatEnableFlag("cache_option_preserves_cache_control")
$compatDisableFlag("cache_option_overrides_cache_control")
$compatEnableDate("2026-11-17");
# Per the Fetch spec, a request's cache mode describes how the cache may use a stored response
# ("no-cache": revalidate it with the origin; "no-store": act as if there is no cache); it says
# nothing about how the response may be cached. Without this flag, `cache: 'no-cache'` and
# `cache: 'no-store'` also set `cf.cacheControl` to "no-cache" and "no-store", which overrides
# the origin's Cache-Control (for example, an origin `no-store` gets cached as `no-cache`). When
# enabled, the cache option no longer sets `cf.cacheControl`. To override the origin's
# Cache-Control, omit the `cache` option and use `cf.cacheControl` instead.
}
Loading