Skip to content

Commit

Permalink
Http Request Cache Part 1 (#2434)
Browse files Browse the repository at this point in the history
* Revert "Merge pull request #2432 from cloudflare/revert-2409-jsnell/http-request-cache-part1"

This reverts commit e7cd9f4, reversing
changes made to d1b6269.

* Gate prototypes behind compatability flag for cache: no-store

* Check for a validate function to enable an interposition step in JSG_STRUCT

* Added documentation to explain validate function

* Include the type check within validate

* Address nits

* Update typescript test

* Update typescript overrides

* Refactor JSG to add DYNAMIC TS overrides

* Run formatter

* deleting extra macro definitions

* Update jsg.h

---------

Co-authored-by: Garrett Gu <garrett@cloudflare.com>
  • Loading branch information
AdityaAtulTewari and garrettgu10 authored Aug 30, 2024
1 parent 295128b commit 92c8f1c
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 54 deletions.
29 changes: 22 additions & 7 deletions src/workerd/api/http-test-ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async function assertFetchCacheRejectsError(
(async () => {
const header = { cache: cacheHeader };
const req: RequestInit = header;
await fetch('http://example.org', req);
await fetch('https://example.org', req);
})(),
{
name: errorName,
Expand All @@ -41,23 +41,38 @@ async function assertFetchCacheRejectsError(
}

export const cacheMode = {
async test() {
let cacheModes: Array<RequestCache> = [
async test(ctrl: any, env: any, ctx: any) {
let allowedCacheModes: Array<RequestCache> = [
'default',
'force-cache',
'no-cache',
'no-store',
'only-if-cached',
'reload',
];
assert.strictEqual('cache' in Request.prototype, false);
assert.strictEqual('cache' in Request.prototype, env.CACHE_ENABLED);
{
const req = new Request('https://example.org', {});
assert.strictEqual(req.cache, undefined);
}
for (var cacheMode of cacheModes) {
await assertRequestCacheThrowsError(cacheMode);
await assertFetchCacheRejectsError(cacheMode);
if (!env.CACHE_ENABLED) {
for (var cacheMode of allowedCacheModes) {
await assertRequestCacheThrowsError(cacheMode);
await assertFetchCacheRejectsError(cacheMode);
}
} else {
for (var cacheMode of allowedCacheModes) {
await assertRequestCacheThrowsError(
cacheMode,
'TypeError',
'Unsupported cache mode: ' + cacheMode
);
await assertFetchCacheRejectsError(
cacheMode,
'TypeError',
'Unsupported cache mode: ' + cacheMode
);
}
}
},
};
18 changes: 16 additions & 2 deletions src/workerd/api/http-test-ts.ts-wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,24 @@ const unitTests :Workerd.Config = (
( name = "worker", esModule = embed "http-test-ts.js" )
],
bindings = [
( name = "SERVICE", service = "http-test" )
( name = "SERVICE", service = "http-test" ),
( name = "CACHE_ENABLED", json = "false" ),
],
compatibilityDate = "2023-08-01",
compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers"],
compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers", "cache_option_disabled"],
)
),
( name = "http-test-cache-option-enabled",
worker = (
modules = [
( name = "worker-cache-enabled", esModule = embed "http-test-ts.js" )
],
bindings = [
( name = "SERVICE", service = "http-test-cache-option-enabled" ),
( name = "CACHE_ENABLED", json = "true" ),
],
compatibilityDate = "2023-08-01",
compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers", "cache_option_enabled"],
)
),
],
Expand Down
98 changes: 83 additions & 15 deletions src/workerd/api/http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ export const inspect = {
body: 'message',
headers: { 'Content-Type': 'text/plain' },
});
assert.strictEqual(
util.inspect(request),
`Request {
if (env.CACHE_ENABLED) {
assert.strictEqual(
util.inspect(request),
`Request {
method: 'POST',
url: 'http://placeholder',
headers: Headers(1) { 'content-type' => 'text/plain', [immutable]: false },
Expand All @@ -153,6 +154,7 @@ export const inspect = {
cf: undefined,
integrity: '',
keepalive: false,
cache: undefined,
body: ReadableStream {
locked: false,
[state]: 'readable',
Expand All @@ -161,7 +163,30 @@ export const inspect = {
},
bodyUsed: false
}`
);
);
} else {
assert.strictEqual(
util.inspect(request),
`Request {
method: 'POST',
url: 'http://placeholder',
headers: Headers(1) { 'content-type' => 'text/plain', [immutable]: false },
redirect: 'follow',
fetcher: null,
signal: AbortSignal { aborted: false, reason: undefined, onabort: null },
cf: undefined,
integrity: '',
keepalive: false,
body: ReadableStream {
locked: false,
[state]: 'readable',
[supportsBYOB]: true,
[length]: 7n
},
bodyUsed: false
}`
);
}

// Check response with immutable headers
const response = await env.SERVICE.fetch('http://placeholder/not-found');
Expand Down Expand Up @@ -252,7 +277,7 @@ async function assertFetchCacheRejectsError(
) {
await assert.rejects(
(async () => {
await fetch('http://example.org', { cache: cacheHeader });
await fetch('https://example.org', { cache: cacheHeader });
})(),
{
name: errorName,
Expand All @@ -262,19 +287,62 @@ async function assertFetchCacheRejectsError(
}

export const cacheMode = {
async test() {
assert.strictEqual('cache' in Request.prototype, false);
async test(ctrl, env, ctx) {
assert.strictEqual('cache' in Request.prototype, env.CACHE_ENABLED);
{
const req = new Request('https://example.org', {});
assert.strictEqual(req.cache, undefined);
}
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');
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');
} 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'
);
}
},
};
17 changes: 15 additions & 2 deletions src/workerd/api/http-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,24 @@ const unitTests :Workerd.Config = (
( name = "worker", esModule = embed "http-test.js" )
],
bindings = [
( name = "SERVICE", service = "http-test" )
( name = "SERVICE", service = "http-test" ),
( name = "CACHE_ENABLED", json = "false" ),
],
compatibilityDate = "2023-08-01",
compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers"],
compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers", "cache_option_disabled"],
)
),
( name = "http-test-cache-option-enabled",
worker = (
modules = [
( name = "worker-cache-enabled", esModule = embed "http-test.js" )
],
bindings = [
( name = "SERVICE", service = "http-test-cache-option-enabled" ),
( name = "CACHE_ENABLED", json = "true" ),
],
compatibilityDate = "2023-08-01",
compatibilityFlags = ["nodejs_compat", "service_binding_extra_handlers", "cache_option_enabled"],
))
],
);
53 changes: 51 additions & 2 deletions src/workerd/api/http.c++
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ void requireValidHeaderValue(kj::StringPtr value) {
}
}

Request::CacheMode getCacheModeFromName(kj::StringPtr value) {
if (value == "no-store") return Request::CacheMode::NOSTORE;
if (value == "no-cache") return Request::CacheMode::NOCACHE;
JSG_FAIL_REQUIRE(TypeError, kj::str("Unsupported cache mode: ", value));
}

jsg::Optional<kj::StringPtr> getCacheModeName(Request::CacheMode mode) {
switch (mode) {
case (Request::CacheMode::NONE):
return kj::none;
case (Request::CacheMode::NOCACHE):
return "no-cache"_kj;
case (Request::CacheMode::NOSTORE):
return "no-store"_kj;
}
KJ_UNREACHABLE;
}

} // namespace

Headers::Headers(jsg::Dict<jsg::ByteString, jsg::ByteString> dict): guard(Guard::NONE) {
Expand Down Expand Up @@ -877,6 +895,13 @@ jsg::Ref<Request> Request::coerce(
: Request::constructor(js, kj::mv(input), kj::mv(init));
}

jsg::Optional<kj::StringPtr> Request::getCache(jsg::Lock& js) {
return getCacheModeName(cacheMode);
}
Request::CacheMode Request::getCacheMode() {
return cacheMode;
}

jsg::Ref<Request> Request::constructor(
jsg::Lock& js, Request::Info input, jsg::Optional<Request::Initializer> init) {
kj::String url;
Expand All @@ -887,6 +912,7 @@ jsg::Ref<Request> Request::constructor(
CfProperty cf;
kj::Maybe<Body::ExtractedBody> body;
Redirect redirect = Redirect::FOLLOW;
CacheMode cacheMode = CacheMode::NONE;

KJ_SWITCH_ONEOF(input) {
KJ_CASE_ONEOF(u, kj::String) {
Expand Down Expand Up @@ -952,6 +978,7 @@ jsg::Ref<Request> Request::constructor(
body = Body::ExtractedBody((oldJsBody)->detach(js), oldRequest->getBodyBuffer(js));
}
}
cacheMode = oldRequest->getCacheMode();
redirect = oldRequest->getRedirectEnum();
fetcher = oldRequest->getFetcher();
signal = oldRequest->getSignal();
Expand Down Expand Up @@ -1029,6 +1056,10 @@ jsg::Ref<Request> Request::constructor(
"response status code).");
}

KJ_IF_SOME(c, initDict.cache) {
cacheMode = getCacheModeFromName(c);
}

if (initDict.method != kj::none || initDict.body != kj::none) {
// We modified at least one of the method or the body. In this case, we enforce the
// spec rule that GET/HEAD requests cannot have bodies. (On the other hand, if neither
Expand All @@ -1043,6 +1074,7 @@ jsg::Ref<Request> Request::constructor(
KJ_CASE_ONEOF(otherRequest, jsg::Ref<Request>) {
method = otherRequest->method;
redirect = otherRequest->redirect;
cacheMode = otherRequest->cacheMode;
fetcher = otherRequest->getFetcher();
signal = otherRequest->getSignal();
headers = jsg::alloc<Headers>(*otherRequest->headers);
Expand All @@ -1063,7 +1095,7 @@ jsg::Ref<Request> Request::constructor(

// TODO(conform): If `init` has a keepalive flag, pass it to the Body constructor.
return jsg::alloc<Request>(method, url, redirect, KJ_ASSERT_NONNULL(kj::mv(headers)),
kj::mv(fetcher), kj::mv(signal), kj::mv(cf), kj::mv(body));
kj::mv(fetcher), kj::mv(signal), kj::mv(cf), kj::mv(body), cacheMode);
}

jsg::Ref<Request> Request::clone(jsg::Lock& js) {
Expand Down Expand Up @@ -1147,6 +1179,16 @@ kj::Maybe<kj::String> Request::serializeCfBlobJson(jsg::Lock& js) {
return cf.serialize(js);
}

void RequestInitializerDict::validate(jsg::Lock& js) {
KJ_IF_SOME(c, cache) {
// Check compatability flag
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));
}
}

void Request::serialize(jsg::Lock& js,
jsg::Serializer& serializer,
const jsg::TypeHandler<RequestInitializerDict>& initDictHandler) {
Expand Down Expand Up @@ -1181,9 +1223,11 @@ void Request::serialize(jsg::Lock& js,

.cf = cf.getRef(js),

.cache = getCacheModeName(cacheMode).map(
[](kj::StringPtr name) -> kj::String { return kj::str(name); }),

// .mode is unimplemented
// .credentials is unimplemented
// .cache is unimplemented
// .referrer is unimplemented
// .referrerPolicy is unimplemented
// .integrity is required to be empty
Expand Down Expand Up @@ -1753,6 +1797,11 @@ jsg::Promise<jsg::Ref<Response>> fetchImplNoOutputLock(jsg::Lock& js,
kj::HttpHeaders headers(ioContext.getHeaderTable());
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);

kj::String url =
uriEncodeControlChars(urlList.back().toString(kj::Url::HTTP_PROXY_REQUEST).asBytes());

Expand Down
Loading

0 comments on commit 92c8f1c

Please sign in to comment.