mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 09:45:06 +02:00
If the cURL request completes with anything other than CURLE_OK, we must not keep the cache entry. For example, if the server's connection closes while transferring data, we receive CURLE_PARTIAL_FILE. We don't want this cache entry to be treated as valid in a subsequent request.
474 lines
17 KiB
HTML
474 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<script src="../include.js"></script>
|
|
<script>
|
|
// RequestServer custom headers.
|
|
const TEST_CACHE_ENABLED_HEADER = "X-Ladybird-Enable-Disk-Cache";
|
|
const TEST_CACHE_STATUS_HEADER = "X-Ladybird-Disk-Cache-Status";
|
|
const TEST_CACHE_REVALIDATION_STATUS_HEADER = "X-Ladybird-Revalidation-Status";
|
|
const TEST_CACHE_REQUEST_TIME_OFFSET = "X-Ladybird-Request-Time-Offset";
|
|
|
|
// http-test-server custom headers.
|
|
const TEST_CACHE_RESPOND_WITH_INCOMPLETE_RESPONSE = "X-Ladybird-Respond-With-Incomplete-Response";
|
|
const TEST_CACHE_RESPOND_WITH_NOT_MODIFIED = "X-Ladybird-Respond-With-Not-Modified";
|
|
|
|
const ACCESS_CONTROL_ALLOW_HEADERS = [
|
|
TEST_CACHE_ENABLED_HEADER,
|
|
TEST_CACHE_REQUEST_TIME_OFFSET,
|
|
TEST_CACHE_RESPOND_WITH_INCOMPLETE_RESPONSE,
|
|
TEST_CACHE_RESPOND_WITH_NOT_MODIFIED,
|
|
].join(", ");
|
|
|
|
const ACCESS_CONTROL_EXPOSE_HEADERS = [TEST_CACHE_STATUS_HEADER, TEST_CACHE_REVALIDATION_STATUS_HEADER].join(", ");
|
|
|
|
const server = httpTestServer();
|
|
|
|
let anyTestFailed = false;
|
|
let lastTestPath = null;
|
|
|
|
async function createRequest(path, options) {
|
|
lastTestPath = path;
|
|
|
|
if (typeof options === "undefined") {
|
|
options = {};
|
|
}
|
|
if (!options.method) {
|
|
options.method = "GET";
|
|
}
|
|
if (!options.status) {
|
|
options.status = 200;
|
|
}
|
|
if (!options.headers) {
|
|
options.headers = {};
|
|
}
|
|
|
|
await server.createEcho("OPTIONS", path, {
|
|
status: 200,
|
|
headers: {
|
|
"Access-Control-Allow-Headers": ACCESS_CONTROL_ALLOW_HEADERS,
|
|
"Access-Control-Allow-Methods": options.method,
|
|
"Access-Control-Allow-Origin": location.origin,
|
|
},
|
|
});
|
|
|
|
options.headers["Access-Control-Allow-Origin"] = location.origin;
|
|
options.headers["Access-Control-Expose-Headers"] = ACCESS_CONTROL_EXPOSE_HEADERS;
|
|
|
|
return server.createEcho(options.method, path, {
|
|
status: options.status,
|
|
headers: options.headers,
|
|
reflect_headers_in_body: true,
|
|
});
|
|
}
|
|
|
|
async function cacheFetch(url, options) {
|
|
if (typeof options === "undefined") {
|
|
options = {};
|
|
}
|
|
if (!options.method) {
|
|
options.method = "GET";
|
|
}
|
|
if (!options.headers) {
|
|
options.headers = {};
|
|
}
|
|
|
|
options.headers[TEST_CACHE_ENABLED_HEADER] = "1";
|
|
|
|
return fetch(url, {
|
|
method: options.method,
|
|
headers: options.headers,
|
|
mode: "cors",
|
|
});
|
|
}
|
|
|
|
function expectCacheStatus(url, response, status) {
|
|
const result = response.headers.get(TEST_CACHE_STATUS_HEADER);
|
|
|
|
if (result !== status) {
|
|
println(`Expected ${url} to contain a cache status of '${status}': received: '${result}'`);
|
|
anyTestFailed = true;
|
|
}
|
|
}
|
|
|
|
function expectRevalidationStatus(url, response, status) {
|
|
const result = response.headers.get(TEST_CACHE_REVALIDATION_STATUS_HEADER);
|
|
|
|
if (result !== status) {
|
|
println(`Expected ${url} to contain a revalidation status of '${status}': received: '${result}'`);
|
|
anyTestFailed = true;
|
|
}
|
|
}
|
|
|
|
async function runTests() {
|
|
let url, response;
|
|
|
|
// Non-GET/HEAD requests are not cached.
|
|
await (async () => {
|
|
for (const method of ["POST", "PUT", "DELETE"]) {
|
|
url = await createRequest(`/cache-test/${method.toLowerCase()}`, {
|
|
method: method,
|
|
headers: {
|
|
"Cache-Control": "max-age=999",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url, { method });
|
|
expectCacheStatus(url, response, "not-cached");
|
|
}
|
|
})();
|
|
|
|
// Responses without a Cache-Control or Expires header are not cached.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/missing-headers");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "not-cached");
|
|
})();
|
|
|
|
// Responses with a no-store directive are not cached.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/no-store", {
|
|
headers: {
|
|
"Cache-Control": "no-store",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "not-cached");
|
|
})();
|
|
|
|
// Responses with only a max-age directive are cached.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/max-age", {
|
|
headers: {
|
|
"Cache-Control": "max-age=5",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
})();
|
|
|
|
// Responses with an age less than their max-age directive are cached.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/max-age-fresh", {
|
|
headers: {
|
|
Age: "2",
|
|
"Cache-Control": "max-age=5",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
})();
|
|
|
|
// Responses with an age equal to their max-age directive are not cached.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/max-age-expired", {
|
|
headers: {
|
|
Age: "5",
|
|
"Cache-Control": "max-age=5",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "not-cached");
|
|
})();
|
|
|
|
// Incomplete responses are not cached.
|
|
await (async () => {
|
|
url = await createRequest(`/cache-test/incomplete`, {
|
|
headers: {
|
|
"Cache-Control": "max-age=999",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_RESPOND_WITH_INCOMPLETE_RESPONSE]: "1",
|
|
},
|
|
});
|
|
|
|
try {
|
|
await response.text();
|
|
|
|
println(`Expected ${url} to result in an error while reading the response body`);
|
|
anyTestFailed = true;
|
|
} catch (e) {}
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
})();
|
|
|
|
// Expired responses are cached until their max-age directive is reached.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/expired-and-refreshed", {
|
|
headers: {
|
|
Age: "2",
|
|
"Cache-Control": "max-age=5",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
|
|
// Expired responses are cached until their max-age directive is reached. A must-revalidate cache directive
|
|
// without a revalidation attribute results in the cache being refreshed.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/expired-and-refreshed-due-to-missing-revalidation-attributes", {
|
|
headers: {
|
|
Age: "2",
|
|
"Cache-Control": "max-age=5,must-revalidate",
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
|
|
// Expired responses are cached until their max-age directive is reached. A must-revalidate cache directive
|
|
// with an invalid revalidation attribute results in the cache being refreshed.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/expired-and-refreshed-due-to-invalid-revalidation-attributes", {
|
|
headers: {
|
|
Age: "2",
|
|
"Cache-Control": "max-age=5,must-revalidate",
|
|
"Last-Modified": new Date().toUTCString(),
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
// By not attaching a TEST_CACHE_RESPOND_WITH_NOT_MODIFIED header, we tell http-test-server to respond to
|
|
// revalidation requests with an HTTP 200 (i.e. revalidation fails).
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
|
|
// Expired responses are cached until their max-age directive is reached. A must-revalidate cache directive
|
|
// with a valid revalidation attribute results in the cache being revalidated.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/expired-and-revalidated", {
|
|
headers: {
|
|
Age: "2",
|
|
"Cache-Control": "max-age=5,must-revalidate",
|
|
"Last-Modified": new Date().toUTCString(),
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: "5",
|
|
[TEST_CACHE_RESPOND_WITH_NOT_MODIFIED]: "1",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
})();
|
|
|
|
// Responses with a no-cache directive must always be revalidated.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/no-cache", {
|
|
headers: {
|
|
"Cache-Control": "no-cache",
|
|
"Last-Modified": new Date().toUTCString(),
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_RESPOND_WITH_NOT_MODIFIED]: "1",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
// By not attaching a TEST_CACHE_RESPOND_WITH_NOT_MODIFIED header, we tell http-test-server to respond to
|
|
// revalidation requests with an HTTP 200 (i.e. revalidation fails).
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
|
|
// Responses without a Cache-Control header may be heuristically cached based on the Last-Modified header.
|
|
await (async () => {
|
|
// Our current heuristic is 10% of the time since the Last-Modified header.
|
|
url = await createRequest("/cache-test/cache-heuristic", {
|
|
headers: {
|
|
"Last-Modified": new Date(Date.now() - 10 * 60 * 60 * 1000).toUTCString(),
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 30 * 60,
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 90 * 60,
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
|
|
// Expired responses are cached until their max-age and stale-while-revalidate directives are reached. The
|
|
// response is successfully revalidated in the background.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/stale-while-revalidate-fresh", {
|
|
headers: {
|
|
"Cache-Control": "max-age=10,stale-while-revalidate=10",
|
|
"Last-Modified": new Date().toUTCString(),
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 12,
|
|
[TEST_CACHE_RESPOND_WITH_NOT_MODIFIED]: "1",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
expectRevalidationStatus(url, response, null);
|
|
|
|
// We must issue another request to inspect the status of the background revalidation request triggered by
|
|
// the previous request.
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 15,
|
|
[TEST_CACHE_RESPOND_WITH_NOT_MODIFIED]: "1",
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
expectRevalidationStatus(url, response, "fresh");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 60,
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
|
|
// Expired responses are cached until their max-age and stale-while-revalidate directives are reached. The
|
|
// response is unsuccessfully revalidated in the background.
|
|
await (async () => {
|
|
url = await createRequest("/cache-test/stale-while-revalidate-expired", {
|
|
headers: {
|
|
"Cache-Control": "max-age=10,stale-while-revalidate=10",
|
|
"Last-Modified": new Date().toUTCString(),
|
|
},
|
|
});
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
|
|
response = await cacheFetch(url);
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
// By not attaching a TEST_CACHE_RESPOND_WITH_NOT_MODIFIED header, we tell http-test-server to respond to
|
|
// revalidation requests with an HTTP 200 (i.e. revalidation fails).
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 12,
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
|
|
// We must issue another request to inspect the status of the background revalidation request triggered by
|
|
// the previous request. Even though revalidation failed, the revalidation request itself will have written
|
|
// a fresh response to disk, thus this request is served from cache.
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 15,
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "read-from-cache");
|
|
expectRevalidationStatus(url, response, "expired");
|
|
|
|
response = await cacheFetch(url, {
|
|
headers: {
|
|
[TEST_CACHE_REQUEST_TIME_OFFSET]: 60,
|
|
},
|
|
});
|
|
expectCacheStatus(url, response, "written-to-cache");
|
|
})();
|
|
}
|
|
|
|
asyncTest(async done => {
|
|
// Disable memory cache to ensure all requests reach RequestServer.
|
|
const httpMemoryCacheWasEnabled = internals.setHttpMemoryCacheEnabled(false);
|
|
|
|
runTests()
|
|
.then(() => {
|
|
if (!anyTestFailed) {
|
|
println("PASS!");
|
|
}
|
|
})
|
|
.catch(e => {
|
|
println(`Caught exception: ${lastTestPath}: ${e}`);
|
|
})
|
|
.finally(() => {
|
|
internals.setHttpMemoryCacheEnabled(httpMemoryCacheWasEnabled);
|
|
done();
|
|
});
|
|
});
|
|
</script>
|