diff --git a/nuxt.config.ts b/nuxt.config.ts index 0f7dc65..888da21 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -4,9 +4,17 @@ import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { + CLOUD_PROBE_PATH_PREFIXES, + CLOUD_PROBE_PATHS, +} from "./server/constants/cloud-probes"; + const configDir = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); +const probeCacheControl = + "no-store, no-cache, must-revalidate, proxy-revalidate"; + function resolveNuxtNitroErrorHandler(): string { const path = join( dirname(require.resolve("nuxt/package.json")), @@ -38,6 +46,21 @@ export default defineNuxtConfig({ fonts: false }, devtools: { enabled: true }, + /** 探针路径与 `server/middleware/00.cloud-probe.ts` 一致:禁止 CDN/LB 长期缓存探测响应 */ + routeRules: { + ...Object.fromEntries( + CLOUD_PROBE_PATHS.map((path) => [ + path, + { headers: { "cache-control": probeCacheControl } }, + ]), + ), + ...Object.fromEntries( + CLOUD_PROBE_PATH_PREFIXES.flatMap((prefix) => [ + [`${prefix}/**`, { headers: { "cache-control": probeCacheControl } }], + [prefix, { headers: { "cache-control": probeCacheControl } }], + ]), + ), + }, typescript: { tsConfig: { compilerOptions: { diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 6f45afc..0de95d4 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/server/constants/cloud-probes.ts b/server/constants/cloud-probes.ts new file mode 100644 index 0000000..719f08c --- /dev/null +++ b/server/constants/cloud-probes.ts @@ -0,0 +1,42 @@ +/** + * 各云平台 / 容器编排常见 HTTP 探针路径(不含 `/`,避免误伤站点根)。 + * 可按负载均衡控制台实际配置增删;若与业务路由同名请从列表中移除对应项。 + */ +export const CLOUD_PROBE_PATHS = [ + // 通用 / Kubernetes + "/health", + "/healthz", + "/livez", + "/readyz", + "/liveness", + "/readiness", + "/startup", + "/health/live", + "/health/ready", + "/health/startup", + // Spring Actuator(经网关暴露时偶见) + "/actuator/health", + "/actuator/health/liveness", + "/actuator/health/readiness", + // Azure App Service 相关默认探测 + "/robots933456.txt", + // 国内云控制台常见示例静态页 + "/check.html", + "/status.html", + "/ping", + "/status", + "/alive", +] as const; + +/** 阿里云等:健康检查为 `/rpc` 或带后缀路径(如控制台填写的 `/rpc/...`),按前缀匹配 */ +export const CLOUD_PROBE_PATH_PREFIXES = ["/rpc"] as const; + +export const CLOUD_PROBE_PATH_SET = new Set(CLOUD_PROBE_PATHS); + +export function isCloudProbePath(pathname: string): boolean { + if (CLOUD_PROBE_PATH_SET.has(pathname)) return true; + for (const prefix of CLOUD_PROBE_PATH_PREFIXES) { + if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return true; + } + return false; +} diff --git a/server/middleware/00.cloud-probe.ts b/server/middleware/00.cloud-probe.ts new file mode 100644 index 0000000..60f50dd --- /dev/null +++ b/server/middleware/00.cloud-probe.ts @@ -0,0 +1,25 @@ +import { getRequestURL } from "h3"; +import { isCloudProbePath } from "#server/constants/cloud-probes"; + +const METHODS = new Set(["GET", "HEAD"]); + +export default eventHandler((event) => { + if (!METHODS.has(event.method)) return; + + const pathname = getRequestURL(event).pathname; + if (!isCloudProbePath(pathname)) return; + + setHeader(event, "content-type", "text/plain; charset=utf-8"); + setHeader( + event, + "cache-control", + "no-store, no-cache, must-revalidate, proxy-revalidate", + ); + + if (event.method === "HEAD") { + setResponseStatus(event, 200); + return; + } + + return "OK"; +});