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
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 @@ -113,6 +113,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 @@ -876,6 +894,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 @@ -886,6 +911,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 @@ -951,6 +977,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 @@ -1028,6 +1055,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 @@ -1042,6 +1073,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 @@ -1062,7 +1094,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 @@ -1146,6 +1178,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 @@ -1180,9 +1222,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 @@ -1752,6 +1796,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