103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
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 ?? []);
|
|
}
|