Merged in feature/warmup (pull request #1887)
* unified warmup function Approved-by: Linus Flood
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
||||
|
||||
export default async (_request: Request, _context: Context) => {
|
||||
await warmupHotelDataOnLang(Lang.da)
|
||||
return new Response("Warmup success", { status: 200 })
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
schedule: "0 4 * * *",
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
||||
|
||||
export default async (_request: Request, _context: Context) => {
|
||||
await warmupHotelDataOnLang(Lang.de)
|
||||
return new Response("Warmup success", { status: 200 })
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
schedule: "5 4 * * *",
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
||||
|
||||
export default async (_request: Request, _context: Context) => {
|
||||
await warmupHotelDataOnLang(Lang.en)
|
||||
return new Response("Warmup success", { status: 200 })
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
schedule: "10 4 * * *",
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
||||
|
||||
export default async (_request: Request, _context: Context) => {
|
||||
await warmupHotelDataOnLang(Lang.fi)
|
||||
return new Response("Warmup success", { status: 200 })
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
schedule: "15 4 * * *",
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
||||
|
||||
export default async (_request: Request, _context: Context) => {
|
||||
await warmupHotelDataOnLang(Lang.no)
|
||||
return new Response("Warmup success", { status: 200 })
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
schedule: "20 4 * * *",
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
||||
|
||||
export default async (_request: Request, _context: Context) => {
|
||||
await warmupHotelDataOnLang(Lang.sv)
|
||||
return new Response("Warmup success", { status: 200 })
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
schedule: "25 4 * * *",
|
||||
}
|
||||
249
apps/scandic-web/netlify/functions/warmup-background.mts
Normal file
249
apps/scandic-web/netlify/functions/warmup-background.mts
Normal file
@@ -0,0 +1,249 @@
|
||||
import crypto from "crypto"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
import {
|
||||
type WarmupFunctionsKey,
|
||||
warmupKeys,
|
||||
} from "@/services/warmup/warmupKeys"
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
import { timeout } from "@/utils/timeout"
|
||||
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
|
||||
async function WarmupHandler(request: Request, context: Context) {
|
||||
const [_, error] = await safeTry(validateRequest(request, context))
|
||||
|
||||
if (error) {
|
||||
if (error instanceof Error) {
|
||||
switch (error.message as ErrorCode) {
|
||||
case ErrorCodes.WARMUP_DISABLED:
|
||||
console.warn("[WARMUP] Warmup is disabled")
|
||||
return
|
||||
case ErrorCodes.URLS_DONT_MATCH:
|
||||
// This is expected, this webhook will be called for all deployments
|
||||
// and we only want to warmup the ones that match our URL
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.error("[WARMUP] Warmup failed", error)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[WARMUP] Request is valid, starting warmup")
|
||||
await performWarmup(context)
|
||||
console.log("[WARMUP] Warmup completed")
|
||||
}
|
||||
|
||||
async function validateRequest(
|
||||
request: Request,
|
||||
context: Context
|
||||
): Promise<true> {
|
||||
if (request.method !== "POST") {
|
||||
throw new Error(ErrorCodes.METHOD_NOT_ALLOWED)
|
||||
}
|
||||
|
||||
const warmupEnabled =
|
||||
true ||
|
||||
process.env.WARMUP_ENABLED === "true" ||
|
||||
Netlify.env.get("WARMUP_ENABLED") === "true"
|
||||
|
||||
if (!warmupEnabled) {
|
||||
throw new Error(ErrorCodes.WARMUP_DISABLED)
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
throw new Error(ErrorCodes.MISSING_BODY)
|
||||
}
|
||||
|
||||
const body = await request.text()
|
||||
console.log("[WARMUP] Warmup body", body)
|
||||
const deployment = JSON.parse(body) as DeploymentInfo
|
||||
if (!deployment) {
|
||||
throw new Error(ErrorCodes.UNABLE_TO_PARSE_DEPLOYMENT_INFO)
|
||||
}
|
||||
|
||||
const deployedUrl = deployment.deploy_ssl_url
|
||||
if (deployedUrl !== context.url.origin) {
|
||||
throw new Error(ErrorCodes.URLS_DONT_MATCH)
|
||||
}
|
||||
|
||||
console.log("[WARMUP] Warmup request", deployment)
|
||||
let signature: string
|
||||
try {
|
||||
const headerValue = request.headers.get("x-webhook-signature")
|
||||
if (!headerValue) {
|
||||
throw new Error(ErrorCodes.MISSING_SIGNATURE_HEADER)
|
||||
}
|
||||
signature = headerValue
|
||||
} catch (e) {
|
||||
console.warn("[WARMUP] Failed to parse signature", e)
|
||||
throw new Error(ErrorCodes.FAILED_TO_PARSE_SIGNATURE)
|
||||
}
|
||||
|
||||
if (!signature) {
|
||||
throw new Error(ErrorCodes.MISSING_SIGNATURE)
|
||||
}
|
||||
|
||||
const isValid = await validateSignature(signature, body)
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error(ErrorCodes.INVALID_SIGNATURE)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function performWarmup(context: Context) {
|
||||
for (const key of warmupKeys) {
|
||||
console.log("[WARMUP] Warming up cache", key)
|
||||
await callWarmup(key, context)
|
||||
// allow api to catch up
|
||||
await timeout(1000)
|
||||
}
|
||||
}
|
||||
|
||||
async function callWarmup(key: WarmupFunctionsKey, context: Context) {
|
||||
const baseUrl = context.url.origin
|
||||
|
||||
const url = new URL("/api/web/warmup", baseUrl)
|
||||
url.searchParams.set("key", key)
|
||||
|
||||
const warmupToken =
|
||||
process.env.WARMUP_TOKEN || Netlify.env.get("WARMUP_TOKEN")
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
cache: "no-store",
|
||||
Authorization: `Bearer ${warmupToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
`[WARMUP] Warmup failed '${url.href}' with error: ${response.status}: ${response.statusText}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[WARMUP] Warmup failed '${url.href}' with error: ${error instanceof Error ? error.message : error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default WarmupHandler
|
||||
export const config: Config = {
|
||||
method: "POST",
|
||||
}
|
||||
|
||||
async function validateSignature(token: string, buffer: string) {
|
||||
try {
|
||||
const secret =
|
||||
process.env.WARMUP_SIGNATURE_SECRET ||
|
||||
Netlify.env.get("WARMUP_SIGNATURE_SECRET")
|
||||
|
||||
if (!secret) {
|
||||
throw new Error("WARMUP_SIGNATURE_SECRET is not set")
|
||||
}
|
||||
|
||||
const options: jwt.VerifyOptions = {
|
||||
issuer: "netlify",
|
||||
algorithms: ["HS256"],
|
||||
}
|
||||
const decoded = jwt.verify(token, secret, options)
|
||||
const hashedBody = crypto.createHash("sha256").update(buffer).digest("hex")
|
||||
|
||||
if (!hasSha256(decoded)) {
|
||||
console.error(
|
||||
"[WARMUP] Decoded jwt does not contain sha256, unable to verify signature"
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return decoded.sha256 === hashedBody
|
||||
} catch (error) {
|
||||
console.error("[WARMUP] Failed to validate signature", error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function hasSha256(decoded: unknown): decoded is { sha256: string } {
|
||||
return (
|
||||
typeof decoded === "object" &&
|
||||
decoded !== null &&
|
||||
"sha256" in decoded &&
|
||||
typeof (decoded as { sha256: unknown }).sha256 === "string"
|
||||
)
|
||||
}
|
||||
|
||||
type DeploymentInfo = {
|
||||
id: string
|
||||
site_id: string
|
||||
build_id: string
|
||||
state: string
|
||||
name: string
|
||||
url: string
|
||||
ssl_url: string
|
||||
admin_url: string
|
||||
deploy_url: string
|
||||
deploy_ssl_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
user_id: string
|
||||
error_message: string | null
|
||||
required: any[]
|
||||
required_functions: any[]
|
||||
commit_ref: string
|
||||
review_id: string | null
|
||||
branch: string
|
||||
commit_url: string
|
||||
skipped: any
|
||||
locked: any
|
||||
title: string
|
||||
commit_message: string | null
|
||||
review_url: string | null
|
||||
published_at: string | null
|
||||
context: string
|
||||
deploy_time: number
|
||||
available_functions: unknown[]
|
||||
screenshot_url: string | null
|
||||
committer: string
|
||||
skipped_log: any
|
||||
manual_deploy: boolean
|
||||
plugin_state: string
|
||||
lighthouse_plugin_scores: any
|
||||
links: {
|
||||
permalink: string
|
||||
alias: string
|
||||
branch: string | null
|
||||
}
|
||||
framework: string
|
||||
entry_path: string | null
|
||||
views_count: number | null
|
||||
function_schedules: {
|
||||
cron: string
|
||||
name: string
|
||||
}[]
|
||||
public_repo: boolean
|
||||
pending_review_reason: string | null
|
||||
lighthouse: any
|
||||
edge_functions_present: boolean
|
||||
expires_at: string | null
|
||||
blobs_region: string
|
||||
}
|
||||
|
||||
const ErrorCodes = {
|
||||
DEPLOYMENT_NOT_FOUND: "DEPLOYMENT NOT FOUND",
|
||||
FAILED_TO_PARSE_SIGNATURE: "FAILED TO PARSE SIGNATURE",
|
||||
INVALID_DEPLOYMENT_STATE: "INVALID DEPLOYMENT STATE",
|
||||
INVALID_SIGNATURE: "INVALID SIGNATURE",
|
||||
METHOD_NOT_ALLOWED: "METHOD NOT ALLOWED",
|
||||
MISSING_BODY: "MISSING BODY",
|
||||
MISSING_SIGNATURE_HEADER: "MISSING SIGNATURE HEADER",
|
||||
MISSING_SIGNATURE: "MISSING SIGNATURE",
|
||||
UNABLE_TO_PARSE_DEPLOYMENT_INFO: "UNABLE TO PARSE DEPLOYMENT INFO",
|
||||
URLS_DONT_MATCH: "URLS DONT MATCH",
|
||||
WARMUP_DISABLED: "WARMUP IS DISABLED",
|
||||
} as const
|
||||
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
||||
@@ -0,0 +1,14 @@
|
||||
import { performWarmup } from "./warmup-background.mjs"
|
||||
|
||||
import type { Config, Context } from "@netlify/functions"
|
||||
|
||||
async function WarmupHandler(_request: Request, context: Context) {
|
||||
console.log("[WARMUP] Starting scheduled warmup")
|
||||
await performWarmup(context)
|
||||
console.log("[WARMUP] Scheduled warmup completed")
|
||||
}
|
||||
|
||||
export default WarmupHandler
|
||||
export const config: Config = {
|
||||
schedule: "0 4 * * *",
|
||||
}
|
||||
Reference in New Issue
Block a user