in cloudflare/src/main.ts [53:234]
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const allowedMethods = ["GET", "HEAD", "OPTIONS"];
if (allowedMethods.indexOf(request.method) === -1) return new Response("Method Not Allowed", { status: 405 });
if (request.method === "OPTIONS") {
return new Response(null, { headers: { "allow": allowedMethods.join(", ") } })
}
let triedIndex = false;
const url = new URL(request.url);
const healthCheck = isHealthCheck(env, url);
if (healthCheck) {
if (env.HEALTHCHECK_UA_REGEXP && !request.headers.get("user-agent")?.match(env.HEALTHCHECK_UA_REGEXP)) {
return new Response("File Not Found", { status: 404 });
}
url.pathname = '/ping';
}
// Trim the first part of the request
url.hostname = url.hostname.replace(/^[^.]+\./, '');
let response: Response | undefined;
if (DEBUG) {
console.debug(`Request URL: ${url.toString()}`);
}
const shouldCache = env.CACHE_CONTROL !== "no-store" && !healthCheck;
const cache = caches.default;
const cacheKey = new Request(url.toString(), request);
if (shouldCache) {
response = await cache.match(cacheKey);
}
let range: ParsedRange | undefined;
if (!response || !(response.ok || response.status == 304)) {
if (DEBUG) {
console.warn(`Cache MISS for ${url.toString()}`);
}
let path = (env.PATH_PREFIX || "") + decodeURIComponent(url.pathname);
if (path.endsWith("/")) {
if (env.INDEX_FILE && env.INDEX_FILE !== "") {
path += env.INDEX_FILE;
triedIndex = true;
}
}
if (path !== "/" && path.startsWith("/")) {
path = path.substring(1);
}
let file: R2Object | R2ObjectBody | null | undefined;
// Range handling
if (request.method === "GET") {
const rangeHeader = request.headers.get("range");
if (rangeHeader) {
file = await env.R2_BUCKET.head(path);
if (!file) return new Response("File Not Found", { status: 404 });
const parsedRanges = parseRange(file.size, rangeHeader);
// R2 only supports 1 range at the moment, reject if there is more than one
if (parsedRanges !== -1 && parsedRanges !== -2 && parsedRanges.length === 1 && parsedRanges.type === "bytes") {
let firstRange = parsedRanges[0];
range = file.size === (firstRange.end + 1) ? { suffix: file.size - firstRange.start } : {
offset: firstRange.start,
length: firstRange.end - firstRange.start + 1
};
} else {
return new Response("Range Not Satisfiable", { status: 416 });
}
}
}
// Etag/If-(Not)-Match handling
// R2 requires that etag checks must not contain quotes, and the S3 spec only allows one etag
// This silently ignores invalid or weak (W/) headers
const getHeaderEtag = (header: string | null) => header?.trim().replace(/^['"]|['"]$/g, "");
const ifMatch = getHeaderEtag(request.headers.get("if-match"));
const ifNoneMatch = getHeaderEtag(request.headers.get("if-none-match"));
const ifModifiedSince = Date.parse(request.headers.get("if-modified-since") || "");
const ifUnmodifiedSince = Date.parse(request.headers.get("if-unmodified-since") || "");
const ifRange = request.headers.get("if-range");
if (range && ifRange && file) {
const maybeDate = Date.parse(ifRange);
if (isNaN(maybeDate) || new Date(maybeDate) > file.uploaded) {
// httpEtag already has quotes, no need to use getHeaderEtag
if (ifRange.startsWith("W/") || ifRange !== file.httpEtag) range = undefined;
}
}
if (ifMatch || ifUnmodifiedSince) {
file = await env.R2_BUCKET.get(path, {
onlyIf: {
etagMatches: ifMatch,
uploadedBefore: ifUnmodifiedSince ? new Date(ifUnmodifiedSince) : undefined
}, range
});
if (file && !hasBody(file)) {
return new Response("Precondition Failed", { status: 412 });
}
}
if (ifNoneMatch || ifModifiedSince) {
// if-none-match overrides if-modified-since completely
if (ifNoneMatch) {
file = await env.R2_BUCKET.get(path, { onlyIf: { etagDoesNotMatch: ifNoneMatch }, range });
} else if (ifModifiedSince) {
file = await env.R2_BUCKET.get(path, { onlyIf: { uploadedAfter: new Date(ifModifiedSince) }, range });
}
if (file && !hasBody(file)) {
return new Response(null, { status: 304 });
}
}
file = request.method === "HEAD"
? await env.R2_BUCKET.head(path)
: ((file && hasBody(file)) ? file : await env.R2_BUCKET.get(path, { range }));
let notFound: boolean = false;
if (file === null) {
if (env.INDEX_FILE && triedIndex) {
// remove the index file since it doesnt exist
path = path.substring(0, path.length - env.INDEX_FILE.length)
}
if (env.NOTFOUND_FILE && env.NOTFOUND_FILE != "") {
notFound = true;
path = env.NOTFOUND_FILE;
file = request.method === "HEAD"
? await env.R2_BUCKET.head(path)
: await env.R2_BUCKET.get(path);
}
// if its still null, either 404 is disabled or that file wasn't found either
// this isn't an else because then there would have to be two of them
if (file == null) {
return new Response("File Not Found", { status: 404 });
}
}
response = new Response((hasBody(file) && file.size !== 0) ? file.body : null, {
status: notFound ? 404 : (range ? 206 : 200),
headers: {
"accept-ranges": "bytes",
"access-control-allow-origin": env.ALLOWED_ORIGINS || "",
"etag": notFound ? "" : file.httpEtag,
// if the 404 file has a custom cache control, we respect it
"cache-control": file.httpMetadata?.cacheControl ?? (notFound || healthCheck ? "" : env.CACHE_CONTROL || ""),
"expires": file.httpMetadata?.cacheExpiry?.toUTCString() ?? "",
"last-modified": notFound ? "" : file.uploaded.toUTCString(),
"content-encoding": file.httpMetadata?.contentEncoding ?? "",
"content-type": file.httpMetadata?.contentType ?? "application/octet-stream",
"content-language": file.httpMetadata?.contentLanguage ?? "",
"content-disposition": file.httpMetadata?.contentDisposition ?? "",
"content-range": (range && !notFound ? getRangeHeader(range, file.size) : ""),
"content-length": (range && !notFound ? (rangeHasLength(range) ? range.length : range.suffix) : file.size).toString(),
"cross-origin-resource-policy": "cross-origin",
}
});
if (request.method === "GET" && !range && shouldCache && !notFound)
ctx.waitUntil(cache.put(cacheKey, response.clone()));
} else {
if (DEBUG) {
console.info(`Cache HIT for ${url.toString()}`);
}
}
return response;
},