From 6cead80e17f61243f80bd805ee88a3fb6dabb0d8 Mon Sep 17 00:00:00 2001 From: AdityaAtulTewari Date: Fri, 20 Sep 2024 17:32:39 -0500 Subject: [PATCH] Add http cache mode autogate, initialize cache control headers, repeat of #2074 (#2425) --- src/workerd/api/BUILD.bazel | 13 +++- src/workerd/api/http-test-ts.ts | 51 ++++++++++++- src/workerd/api/http-test.js | 103 ++++++++++++++------------- src/workerd/api/http.c++ | 64 +++++++++++++++-- src/workerd/io/io-thread-context.c++ | 1 + src/workerd/io/io-thread-context.h | 1 + 6 files changed, 176 insertions(+), 57 deletions(-) diff --git a/src/workerd/api/BUILD.bazel b/src/workerd/api/BUILD.bazel index 29894f3925e..7713e536263 100644 --- a/src/workerd/api/BUILD.bazel +++ b/src/workerd/api/BUILD.bazel @@ -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") @@ -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 diff --git a/src/workerd/api/http-test-ts.ts b/src/workerd/api/http-test-ts.ts index c9338f01d54..6183ef7030b 100644 --- a/src/workerd/api/http-test-ts.ts +++ b/src/workerd/api/http-test-ts.ts @@ -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, @@ -42,7 +50,7 @@ async function assertFetchCacheRejectsError( export const cacheMode = { async test(ctrl: any, env: any, ctx: any) { - let allowedCacheModes: Array = [ + const allowedCacheModes: RequestCache[] = [ 'default', 'force-cache', 'no-cache', @@ -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', diff --git a/src/workerd/api/http-test.js b/src/workerd/api/http-test.js index 352bee4bd6d..ecb2bb39dec 100644 --- a/src/workerd/api/http-test.js +++ b/src/workerd/api/http-test.js @@ -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 + ); + } } }, }; diff --git a/src/workerd/api/http.c++ b/src/workerd/api/http.c++ index cec81b9bdd0..0764df17daa 100644 --- a/src/workerd/api/http.c++ +++ b/src/workerd/api/http.c++ @@ -10,12 +10,14 @@ #include "system-streams.h" #include "util.h" #include "worker-rpc.h" +#include "workerd/jsg/jsvalue.h" #include #include #include #include #include +#include #include #include #include @@ -1180,7 +1182,41 @@ void Request::shallowCopyHeadersTo(kj::HttpHeaders& out) { } kj::Maybe 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) { @@ -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)); } } @@ -1802,9 +1841,24 @@ jsg::Promise> 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()); diff --git a/src/workerd/io/io-thread-context.c++ b/src/workerd/io/io-thread-context.c++ index 1c20b01dd9c..8c35b9a0000 100644 --- a/src/workerd/io/io-thread-context.c++ +++ b/src/workerd/io/io-thread-context.c++ @@ -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")), diff --git a/src/workerd/io/io-thread-context.h b/src/workerd/io/io-thread-context.h index 698fac99722..35498967d79 100644 --- a/src/workerd/io/io-thread-context.h +++ b/src/workerd/io/io-thread-context.h @@ -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