import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; export const dynamic = "force-dynamic"; // 说明:ONLYOFFICE 文档服务器在编辑过程中会从 `/cache/*` 拉取二进制缓存(例如 Editor.bin)。 // 当我们通过 `/onlyoffice-server/*` 反代文档服务器时,这些 `/cache/*` 请求会落到 Next 上, // 若未额外反代,会导致 404,进而触发 ONLYOFFICE “下载失败(-4)/无法打开文档”。 // 因此这里把 `/cache/*` 同样反代到本机 ONLYOFFICE。 const ONLYOFFICE_INTERNAL_URL = (process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081").replace( /\/+$/, "", ); const stripHopByHopHeaders = (headers: Headers) => { // 说明:Hop-by-hop headers 不应被代理转发/透传 const hopByHop = [ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", ]; for (const key of hopByHop) { headers.delete(key); } }; const pickCacheControl = (pathParts: string[], contentType: string, method: string) => { const m = String(method || "").toUpperCase(); if (m !== "GET" && m !== "HEAD") return "no-store"; if (contentType.includes("text/html")) return "no-store"; const p = `/${(pathParts ?? []).join("/")}`.toLowerCase(); // 说明:/cache 主要是 ONLYOFFICE 运行期二进制缓存,适合短缓存提升性能,但不宜过长。 if (p.includes("editor.bin") || p.endsWith(".bin")) { return "public, max-age=3600, stale-while-revalidate=600"; } return "public, max-age=300, stale-while-revalidate=300"; }; const proxyCache = async (request: NextRequest, pathParts: string[]) => { const incomingUrl = new URL(request.url); const target = new URL( `${ONLYOFFICE_INTERNAL_URL}/cache/${(pathParts ?? []).map(encodeURIComponent).join("/")}`, ); target.search = incomingUrl.search; const headers = new Headers(request.headers); headers.delete("host"); // 说明:避免上游 gzip 后被自动解压但仍带 content-encoding,导致浏览器二次解压错误 headers.set("accept-encoding", "identity"); stripHopByHopHeaders(headers); const method = request.method.toUpperCase(); const hasBody = !["GET", "HEAD"].includes(method); // eslint-disable-next-line @typescript-eslint/no-explicit-any const init: any = { method, headers, body: hasBody ? request.body : undefined, duplex: "half", redirect: "follow", }; const upstream = await fetch(target, init); const outHeaders = new Headers(upstream.headers); stripHopByHopHeaders(outHeaders); outHeaders.delete("content-encoding"); outHeaders.delete("content-length"); outHeaders.set("cache-control", pickCacheControl(pathParts, upstream.headers.get("content-type") || "", request.method)); outHeaders.delete("pragma"); outHeaders.delete("expires"); outHeaders.delete("set-cookie"); return new NextResponse(upstream.body, { status: upstream.status, headers: outHeaders, }); }; type RouteCtx = { params: Promise<{ path: string[] }> }; export async function GET(request: NextRequest, ctx: RouteCtx) { const { path } = await ctx.params; return proxyCache(request, path ?? []); } export async function HEAD(request: NextRequest, ctx: RouteCtx) { const { path } = await ctx.params; return proxyCache(request, path ?? []); } export async function OPTIONS(request: NextRequest, ctx: RouteCtx) { const { path } = await ctx.params; return proxyCache(request, path ?? []); }