Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(functions): optimize grapher worker for the hot code path #2857

Merged
merged 2 commits into from
Oct 25, 2023
Merged
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
87 changes: 56 additions & 31 deletions functions/grapher/[slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,82 @@ export const onRequestGet: PagesFunction = async (context) => {
// Only caveat is that redirects will not be taken into account for some reason; but on the other hand the worker is so simple that it's unlikely to fail.
context.passThroughOnException()

// Redirects handling is performed by the worker, and is done by fetching the (baked) _grapherRedirects.json file.
// That file is a mapping from old slug to new slug.
const getOptionalRedirectForSlug = async (slug: string, baseUrl: URL) => {
const redirects: Record<string, string> = await env.ASSETS.fetch(
new URL("/grapher/_grapherRedirects.json", baseUrl),
{ cf: { cacheTtl: 2 * 60 } }
)
.then((r): Promise<Record<string, string>> => r.json())
.catch((e) => {
console.error("Error fetching redirects", e)
return {}
})
return redirects[slug]
}

const createRedirectResponse = (redirSlug: string, currentUrl: URL) =>
new Response(null, {
status: 302,
headers: { Location: `/grapher/${redirSlug}${currentUrl.search}` },
})

const { request, env, params } = context

const originalSlug = params.slug as string
const url = new URL(request.url)

/**
* REDIRECTS HANDLING:
* We want to optimize for the case where the user visits a page using the correct slug, i.e. there's no redirect.
* That's why:
* 1. We first check if the slug is lowercase. If it's not, we convert it to lowercase _and check for any redirects already_, and send a redirect already.
* 2. If the slug is lowercase, we check if we can find the page at the requested slug. If we can find it, we return it already.
* 3. If we can't find it, we _then_ check if there's a redirect for it. If there is, we redirect to the new page.
*/

// All our grapher slugs are lowercase by convention.
// To allow incoming links that may contain uppercase characters to work, we redirect to the lowercase version.
// We also then resolve redirects using the /grapher/_grapherRedirects.json file.
// We do this before we even know that the page exists, so even if we redirect it might still be a 404, or go into another redirect.
let currentSlug = originalSlug.toLowerCase()
const redirects = await env.ASSETS.fetch(
new URL("/grapher/_grapherRedirects.json", url),
{ cf: { cacheTtl: 2 * 60 } }
)
.then((r): Promise<Record<string, string>> => r.json())
.catch((e) => {
console.error("Error fetching redirects", e)
return {}
})

if (redirects[currentSlug]) {
currentSlug = redirects[currentSlug]
}
const lowerCaseSlug = originalSlug.toLowerCase()
if (lowerCaseSlug !== originalSlug) {
const redirectSlug = await getOptionalRedirectForSlug(
lowerCaseSlug,
url
)

if (currentSlug !== originalSlug) {
const redirUrl = `/grapher/${currentSlug}` + url.search
return new Response(null, {
status: 302,
headers: { Location: redirUrl },
})
return createRedirectResponse(redirectSlug ?? lowerCaseSlug, url)
}

const { search } = url

const grapherPageResp = await env.ASSETS.fetch(url, { redirect: "manual" })

// For local testing
// const grapherPageResp = await fetch(
// `https://ourworldindata.org/grapher/${currentSlug}`,
// { redirect: "manual" }
// )

const grapherPageResp = await env.ASSETS.fetch(url, { redirect: "manual" })

if (grapherPageResp.status === 404) {
// If the request is a 404, we check if there's a redirect for it.
// If there is, we redirect to the new page.
const redirectSlug = await getOptionalRedirectForSlug(originalSlug, url)
if (redirectSlug && redirectSlug !== originalSlug) {
return createRedirectResponse(redirectSlug, url)
} else {
// Otherwise we just return the 404 page.
return grapherPageResp
}
}

// A non-200 status code is most likely a redirect (301 or 302) or a 404, all of which we want to pass through as-is.
// In the case of the redirect, the browser will then request the new URL which will again be handled by this worker.
if (grapherPageResp.status !== 200) return grapherPageResp

const openGraphThumbnailUrl = `/grapher/thumbnail/${currentSlug}.png?imType=og${
search ? "&" + search.slice(1) : ""
const openGraphThumbnailUrl = `/grapher/thumbnail/${lowerCaseSlug}.png?imType=og${
url.search ? "&" + url.search.slice(1) : ""
}`
const twitterThumbnailUrl = `/grapher/thumbnail/${currentSlug}.png?imType=twitter${
search ? "&" + search.slice(1) : ""
const twitterThumbnailUrl = `/grapher/thumbnail/${lowerCaseSlug}.png?imType=twitter${
url.search ? "&" + url.search.slice(1) : ""
}`

// Take the origin (e.g. https://ourworldindata.org) from the canonical URL, which should appear before the image elements.
Expand All @@ -66,7 +91,7 @@ export const onRequestGet: PagesFunction = async (context) => {
// Replace canonical URL, otherwise the preview image will not include the search parameters.
element: (element) => {
const canonicalUrl = element.getAttribute("content")
element.setAttribute("content", canonicalUrl + search)
element.setAttribute("content", canonicalUrl + url.search)
origin = new URL(canonicalUrl).origin
},
})
Expand Down
Loading