Skip to content

Commit

Permalink
Add http cache mode autogate, initialize cache control headers, repea…
Browse files Browse the repository at this point in the history
…t of #2074 (#2425)
  • Loading branch information
AdityaAtulTewari authored Sep 20, 2024
1 parent 80bad7f commit 6cead80
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 57 deletions.
13 changes: 12 additions & 1 deletion src/workerd/api/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
load("//:build/kj_test.bzl", "kj_test")
load("//:build/wd_cc_capnp_library.bzl", "wd_cc_capnp_library")
load("//:build/wd_cc_library.bzl", "wd_cc_library")
Expand Down Expand Up @@ -479,10 +480,20 @@ wd_test(
data = ["tests/js-rpc-test.js"],
)

ts_project(
name = "http-test@ts_project",
srcs = ["http-test-ts.ts"],
allow_js = True,
composite = True,
source_map = True,
tsconfig = "tsconfig.json",
deps = ["//src/node:node.capnp@tsproject"],
)

wd_test(
src = "http-test-ts.ts-wd-test",
args = ["--experimental"],
data = ["http-test-ts.ts"],
data = ["http-test-ts.js"],
)

# Enable GPU tests if experimental GPU support is enabled. Unfortunately, this depends on the right
Expand Down
51 changes: 48 additions & 3 deletions src/workerd/api/http-test-ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
// https://opensource.org/licenses/Apache-2.0

import assert from 'node:assert';
import util from 'node:util';

export default {
async fetch(request: any, env: any, ctx: any) {
const { pathname } = new URL(request.url);
return new Response(null, { status: 404 });
},
};

async function assertRequestCacheThrowsError(
cacheHeader: RequestCache,
Expand Down Expand Up @@ -42,7 +50,7 @@ async function assertFetchCacheRejectsError(

export const cacheMode = {
async test(ctrl: any, env: any, ctx: any) {
let allowedCacheModes: Array<RequestCache> = [
const allowedCacheModes: RequestCache[] = [
'default',
'force-cache',
'no-cache',
Expand All @@ -56,12 +64,49 @@ export const cacheMode = {
assert.strictEqual(req.cache, undefined);
}
if (!env.CACHE_ENABLED) {
for (var cacheMode of allowedCacheModes) {
for (const cacheMode of allowedCacheModes) {
await assertRequestCacheThrowsError(cacheMode);
await assertFetchCacheRejectsError(cacheMode);
}
} else {
for (var cacheMode of allowedCacheModes) {
var failureCacheModes: RequestCache[] = [
'default',
'no-cache',
'force-cache',
'only-if-cached',
'reload',
];
{
const req = new Request('https://example.org', { cache: 'no-store' });
assert.strictEqual(req.cache, 'no-store');
}
{
const response = await env.SERVICE.fetch(
'http://placeholder/not-found',
{ cache: 'no-store' }
);
assert.strictEqual(
util.inspect(response),
`Response {
status: 404,
statusText: 'Not Found',
headers: Headers(0) { [immutable]: true },
ok: false,
redirected: false,
url: 'http://placeholder/not-found',
webSocket: null,
cf: undefined,
body: ReadableStream {
locked: false,
[state]: 'readable',
[supportsBYOB]: true,
[length]: 0n
},
bodyUsed: false
}`
);
}
for (const cacheMode of failureCacheModes) {
await assertRequestCacheThrowsError(
cacheMode,
'TypeError',
Expand Down
103 changes: 55 additions & 48 deletions src/workerd/api/http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,61 +288,68 @@ async function assertFetchCacheRejectsError(

export const cacheMode = {
async test(ctrl, env, ctx) {
var failureCases = [
'default',
'force-cache',
'no-cache',
'only-if-cached',
'reload',
'unsupported',
];
assert.strictEqual('cache' in Request.prototype, env.CACHE_ENABLED);
{
const req = new Request('https://example.org', {});
assert.strictEqual(req.cache, undefined);
}
if (!env.CACHE_ENABLED) {
await assertRequestCacheThrowsError('no-store');
await assertRequestCacheThrowsError('no-cache');
await assertRequestCacheThrowsError('no-transform');
await assertRequestCacheThrowsError('unsupported');
await assertFetchCacheRejectsError('no-store');
await assertFetchCacheRejectsError('no-cache');
await assertFetchCacheRejectsError('no-transform');
await assertFetchCacheRejectsError('unsupported');
failureCases.push('no-store');
for (const cacheMode in failureCases) {
await assertRequestCacheThrowsError(cacheMode);
await assertFetchCacheRejectsError(cacheMode);
}
} else {
await assertRequestCacheThrowsError(
'no-store',
'TypeError',
'Unsupported cache mode: no-store'
);
await assertRequestCacheThrowsError(
'no-cache',
'TypeError',
'Unsupported cache mode: no-cache'
);
await assertRequestCacheThrowsError(
'no-transform',
'TypeError',
'Unsupported cache mode: no-transform'
);
await assertRequestCacheThrowsError(
'unsupported',
'TypeError',
'Unsupported cache mode: unsupported'
);
await assertFetchCacheRejectsError(
'no-store',
'TypeError',
'Unsupported cache mode: no-store'
);
await assertFetchCacheRejectsError(
'no-cache',
'TypeError',
'Unsupported cache mode: no-cache'
);
await assertFetchCacheRejectsError(
'no-transform',
'TypeError',
'Unsupported cache mode: no-transform'
);
await assertFetchCacheRejectsError(
'unsupported',
'TypeError',
'Unsupported cache mode: unsupported'
);
{
const req = new Request('https://example.org', { cache: 'no-store' });
assert.strictEqual(req.cache, 'no-store');
}
{
const response = await env.SERVICE.fetch(
'http://placeholder/not-found',
{ cache: 'no-store' }
);
assert.strictEqual(
util.inspect(response),
`Response {
status: 404,
statusText: 'Not Found',
headers: Headers(0) { [immutable]: true },
ok: false,
redirected: false,
url: 'http://placeholder/not-found',
webSocket: null,
cf: undefined,
body: ReadableStream {
locked: false,
[state]: 'readable',
[supportsBYOB]: true,
[length]: 0n
},
bodyUsed: false
}`
);
}
for (const cacheMode in failureCases) {
await assertRequestCacheThrowsError(
cacheMode,
'TypeError',
'Unsupported cache mode: ' + cacheMode
);
await assertFetchCacheRejectsError(
cacheMode,
'TypeError',
'Unsupported cache mode: ' + cacheMode
);
}
}
},
};
64 changes: 59 additions & 5 deletions src/workerd/api/http.c++
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
#include "system-streams.h"
#include "util.h"
#include "worker-rpc.h"
#include "workerd/jsg/jsvalue.h"

#include <workerd/io/features.h>
#include <workerd/io/io-context.h>
#include <workerd/jsg/ser.h>
#include <workerd/jsg/url.h>
#include <workerd/util/abortable.h>
#include <workerd/util/autogate.h>
#include <workerd/util/http-util.h>
#include <workerd/util/mimetype.h>
#include <workerd/util/stream-utils.h>
Expand Down Expand Up @@ -1180,7 +1182,41 @@ void Request::shallowCopyHeadersTo(kj::HttpHeaders& out) {
}

kj::Maybe<kj::String> Request::serializeCfBlobJson(jsg::Lock& js) {
return cf.serialize(js);
if (cacheMode == CacheMode::NONE) {
return cf.serialize(js);
}

CfProperty clone;
KJ_IF_SOME(obj, cf.get(js)) {
(void)obj;
clone = cf.deepClone(js);
} else {
clone = CfProperty(js, js.obj());
}
auto obj = KJ_ASSERT_NONNULL(clone.get(js));

int ttl = 2;
switch (cacheMode) {
case CacheMode::NOSTORE:
ttl = -1;
obj.set(js, "cf-cache-level", js.str("byc"_kjc));
break;
case CacheMode::NOCACHE:
ttl = 0;
case CacheMode::NONE:
KJ_UNREACHABLE;
}

if (obj.has(js, "cacheTtl")) {
jsg::JsValue oldTtl = obj.get(js, "cacheTtl");
JSG_REQUIRE(oldTtl == js.num(ttl), TypeError,
kj::str("CacheTtl: ", oldTtl, ", is not compatible with cache: ",
getCacheModeName(cacheMode).orDefault("none"_kj), " header."));
} else {
obj.set(js, "cacheTtl", js.num(ttl));
}

return clone.serialize(js);
}

void RequestInitializerDict::validate(jsg::Lock& js) {
Expand All @@ -1189,7 +1225,10 @@ void RequestInitializerDict::validate(jsg::Lock& js) {
JSG_REQUIRE(FeatureFlags::get(js).getCacheOptionEnabled(), Error,
kj::str("The 'cache' field on 'RequestInitializerDict' is not implemented."));

JSG_FAIL_REQUIRE(TypeError, kj::str("Unsupported cache mode: ", c));
// Validate that the cache type is valid
auto cacheMode = getCacheModeFromName(c);
JSG_REQUIRE(cacheMode != Request::CacheMode::NOCACHE, TypeError,
kj::str("Unsupported cache mode: ", c));
}
}

Expand Down Expand Up @@ -1802,9 +1841,24 @@ jsg::Promise<jsg::Ref<Response>> fetchImplNoOutputLock(jsg::Lock& js,
jsRequest->shallowCopyHeadersTo(headers);

// If the jsRequest has a CacheMode, we need to handle that here.
// Currently, the only cache mode we support is undefined, but we will soon support
// no-cache and no-store. These additional modes will be hidden behind an autogate.
KJ_ASSERT(jsRequest->getCacheMode() == Request::CacheMode::NONE);
// Currently, the only cache mode we support is undefined and no-store (behind an autogate),
// but we will soon support no-cache.
auto headerIds = ioContext.getHeaderIds();
const auto cacheMode = jsRequest->getCacheMode();
switch (cacheMode) {
case Request::CacheMode::NOSTORE:
case Request::CacheMode::NOCACHE:
if (headers.get(headerIds.cacheControl) == kj::none) {
headers.set(headerIds.cacheControl, "no-cache");
}
if (headers.get(headerIds.pragma) == kj::none) {
headers.set(headerIds.pragma, "no-cache");
}
case Request::CacheMode::NONE:
break;
default:
KJ_UNREACHABLE;
}

kj::String url =
uriEncodeControlChars(urlList.back().toString(kj::Url::HTTP_PROXY_REQUEST).asBytes());
Expand Down
1 change: 1 addition & 0 deletions src/workerd/io/io-thread-context.c++
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ThreadContext::HeaderIdBundle::HeaderIdBundle(kj::HttpHeaderTable::Builder& buil
contentEncoding(builder.add("Content-Encoding")),
cfCacheStatus(builder.add("CF-Cache-Status")),
cacheControl(builder.add("Cache-Control")),
pragma(builder.add("Pragma")),
cfCacheNamespace(builder.add("CF-Cache-Namespace")),
cfKvMetadata(builder.add("CF-KV-Metadata")),
cfR2ErrorHeader(builder.add("CF-R2-Error")),
Expand Down
1 change: 1 addition & 0 deletions src/workerd/io/io-thread-context.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class ThreadContext {
const kj::HttpHeaderId contentEncoding;
const kj::HttpHeaderId cfCacheStatus; // used by cache API implementation
const kj::HttpHeaderId cacheControl;
const kj::HttpHeaderId pragma;
const kj::HttpHeaderId cfCacheNamespace; // used by Cache binding implementation
const kj::HttpHeaderId cfKvMetadata; // used by KV binding implementation
const kj::HttpHeaderId cfR2ErrorHeader; // used by R2 binding implementation
Expand Down

0 comments on commit 6cead80

Please sign in to comment.