Skip to content

Commit

Permalink
Update Request to support cache option (#2073)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed May 3, 2024
1 parent c3c30aa commit bb8c8cd
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 17 deletions.
34 changes: 34 additions & 0 deletions src/workerd/api/http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const inspect = {
});
assert.strictEqual(util.inspect(request),
`Request {
cache: undefined,
keepalive: false,
integrity: '',
cf: undefined,
Expand Down Expand Up @@ -205,3 +206,36 @@ export const inspect = {
await messagePromise;
}
};

export const cacheMode = {
async test() {
{
const req = new Request('https://example.org', { });
assert.strictEqual(req.cache, undefined);
}
{
const req = new Request('https://example.org', { cache: 'no-store' });
assert.strictEqual(req.cache, 'no-store');
}
{
const req = new Request('https://example.org', { cache: 'no-cache' });
assert.strictEqual(req.cache, 'no-cache');
}
assert.throws(() => {
new Request('https://example.org', { cache: 'unsupported' });
}, {
name: 'TypeError',
message: 'Unsupported cache mode: unsupported',
});

// Any value other than undefined is currently not supported
// TODO(soon): The no-store and no-cache values will be supported
// soon, at which time this test will need to be updated.
await assert.rejects((async () => {
await fetch('http://example.org', { cache: 'no-store' });
})(), {
name: 'TypeError',
message: 'Unsupported cache mode: no-store',
});
}
};
44 changes: 43 additions & 1 deletion src/workerd/api/http.c++
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ 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)
Expand Down Expand Up @@ -882,6 +897,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,
Expand All @@ -894,6 +916,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 @@ -943,6 +966,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 @@ -1019,6 +1043,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 @@ -1033,6 +1061,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 @@ -1054,7 +1083,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(cf), kj::mv(body), cacheMode);
}

jsg::Ref<Request> Request::clone(jsg::Lock& js) {
Expand Down Expand Up @@ -1169,6 +1198,10 @@ void Request::serialize(

.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
Expand Down Expand Up @@ -1739,6 +1772,15 @@ jsg::Promise<jsg::Ref<Response>> fetchImplNoOutputLock(
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.
if (jsRequest->getCacheMode() != Request::CacheMode::NONE) {
return js.rejectedPromise<jsg::Ref<Response>>(
js.typeError(kj::str("Unsupported cache mode: ",
KJ_ASSERT_NONNULL(jsRequest->getCache(js)))));
}

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

Expand Down
35 changes: 19 additions & 16 deletions src/workerd/api/http.h
Original file line number Diff line number Diff line change
Expand Up @@ -689,9 +689,10 @@ struct RequestInitializerDict {
jsg::WontImplement credentials;

// In browsers this controls the local browser cache. For Cloudflare Workers it could control the
// Cloudflare edge cache. Note that this setting is different from using the `Cache-Control`
// header since `Cache-Control` would be forwarded to the origin.
jsg::Unimplemented cache;
// Cloudflare edge cache. While the standard defines a number of values for this property, our
// implementation supports only three: undefined (identifying the default caching behavior that
// has been implemented by the runtime), "no-store", and "no-cache".
jsg::Optional<kj::String> cache;

// These control how the `Referer` and `Origin` headers are initialized by the browser.
// Browser-side JavaScript is normally not permitted to set these headers, because servers
Expand Down Expand Up @@ -764,13 +765,21 @@ class Request: public Body {
};
static kj::Maybe<Redirect> tryParseRedirect(kj::StringPtr redirect);

enum class CacheMode {
// CacheMode::NONE is set when cache is undefined. It represents the dafault cache
// mode that workers has supported.
NONE,
NOSTORE,
NOCACHE,
};

Request(kj::HttpMethod method, kj::StringPtr url, Redirect redirect,
jsg::Ref<Headers> headers, kj::Maybe<jsg::Ref<Fetcher>> fetcher,
kj::Maybe<jsg::Ref<AbortSignal>> signal, CfProperty&& cf,
kj::Maybe<Body::ExtractedBody> body)
kj::Maybe<Body::ExtractedBody> body, CacheMode cacheMode = CacheMode::NONE)
: Body(kj::mv(body), *headers), method(method), url(kj::str(url)),
redirect(redirect), headers(kj::mv(headers)), fetcher(kj::mv(fetcher)),
cf(kj::mv(cf)) {
cacheMode(cacheMode), cf(kj::mv(cf)) {
KJ_IF_SOME(s, signal) {
// If the AbortSignal will never abort, assigning it to thisSignal instead ensures
// that the cancel machinery is not used but the request.signal accessor will still
Expand Down Expand Up @@ -865,15 +874,8 @@ class Request: public Body {
// TODO(conform): Won't implement?

// The cache mode determines how HTTP cache is used with the request.
// We currently do not fully implement this. Currently we will explicitly
// throw in the Request constructor if the option is set. For the accessor
// we want it to always just return undefined while it is not implemented.
// The spec does not provide a value to indicate "unimplemented" and all
// of the other values would imply semantics we do not follow. In discussion
// with other implementers with the same issues, it was decided that
// simply returning undefined for these was the best option.
// jsg::JsValue getCache(jsg::Lock& js) { return js.v8Undefined(); }
// TODO(conform): Won't implement?
jsg::Optional<kj::StringPtr> getCache(jsg::Lock& js);
CacheMode getCacheMode();

// We do not implement integrity checking at all. However, the spec says that
// the default value should be an empty string. When the Request object is
Expand Down Expand Up @@ -903,7 +905,6 @@ class Request: public Body {
// JSG_READONLY_PROTOTYPE_PROPERTY(duplex, getDuplex);
// JSG_READONLY_PROTOTYPE_PROPERTY(mode, getMode);
// JSG_READONLY_PROTOTYPE_PROPERTY(credentials, getCredentials);
// JSG_READONLY_PROTOTYPE_PROPERTY(cache, getCache);
JSG_READONLY_PROTOTYPE_PROPERTY(integrity, getIntegrity);
JSG_READONLY_PROTOTYPE_PROPERTY(keepalive, getKeepalive);

Expand All @@ -929,7 +930,6 @@ class Request: public Body {
// JSG_READONLY_INSTANCE_PROPERTY(duplex, getDuplex);
// JSG_READONLY_INSTANCE_PROPERTY(mode, getMode);
// JSG_READONLY_INSTANCE_PROPERTY(credentials, getCredentials);
// JSG_READONLY_INSTANCE_PROPERTY(cache, getCache);
JSG_READONLY_INSTANCE_PROPERTY(integrity, getIntegrity);
JSG_READONLY_INSTANCE_PROPERTY(keepalive, getKeepalive);

Expand All @@ -939,6 +939,7 @@ class Request: public Body {
readonly cf?: Cf;
});
}
JSG_READONLY_PROTOTYPE_PROPERTY(cache, getCache);
}

void serialize(
Expand Down Expand Up @@ -967,6 +968,8 @@ class Request: public Body {
kj::Maybe<jsg::Ref<Fetcher>> fetcher;
kj::Maybe<jsg::Ref<AbortSignal>> signal;

CacheMode cacheMode = CacheMode::NONE;

// The fetch spec definition of Request has a distinction between the "signal" (which is
// an optional AbortSignal passed in with the options), and "this' signal", which is an
// AbortSignal that is always available via the request.signal accessor. When signal is
Expand Down

0 comments on commit bb8c8cd

Please sign in to comment.