M0:照壁 BFF 骨架(Fastify 透明代理:鉴权/幂等/续传头透传、SSE 直通、HJ-3001 映射)及测试与镜像
This commit is contained in:
1 parent
f3058a64c3
commit
8d8d81e757
10 files changed
+2700
No files matched your search
@@ -0,0 +1,103 @@
|
||||
/** 代理核心:鉴权透传、请求转发、SSE 流式透传与错误映射。
|
||||
|
||||
* 照壁不拥有业务规则(service-boundaries.md §3):能否看到的语义一律来自灵台;
|
||||
* 这里只做传输与形状处理——透传凭证/幂等/续传头,流式响应禁缓冲直通。
|
||||
*/
|
||||
import { Readable } from "node:stream";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
/** 需要原样透传的请求头(小写)。 */
|
||||
const FORWARD_REQUEST_HEADERS = [
|
||||
"authorization",
|
||||
"content-type",
|
||||
"accept",
|
||||
"idempotency-key",
|
||||
"last-event-id",
|
||||
"x-trace-id",
|
||||
] as const;
|
||||
|
||||
/** 需要透传的响应头(SSE 关键头由流式分支显式设置)。 */
|
||||
const FORWARD_RESPONSE_HEADERS = ["content-type", "x-trace-id", "retry-after"] as const;
|
||||
|
||||
export interface UpstreamError {
|
||||
statusCode: number;
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** 判定上游响应是否为 SSE 流。 */
|
||||
function isEventStream(contentType: string | null): boolean {
|
||||
return (contentType ?? "").includes("text/event-stream");
|
||||
}
|
||||
|
||||
export async function proxyRequest(
|
||||
req: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
backendUrl: string,
|
||||
): Promise<void> {
|
||||
const headers: Record<string, string> = {};
|
||||
for (const name of FORWARD_REQUEST_HEADERS) {
|
||||
const value = req.headers[name];
|
||||
if (value != null) {
|
||||
headers[name] = Array.isArray(value) ? value.join(", ") : value;
|
||||
}
|
||||
}
|
||||
|
||||
const method = req.method;
|
||||
const body = method === "GET" || method === "HEAD" ? undefined : (req.body as Buffer);
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(`${backendUrl}${req.url}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body && body.length > 0 ? new Uint8Array(body) : undefined,
|
||||
redirect: "manual",
|
||||
});
|
||||
} catch (err) {
|
||||
// 上游不可达:HJ-3001(API 契约 §5 错误结构)
|
||||
req.log.warn({ err: String(err) }, "上游不可达");
|
||||
await reply.code(502).send({
|
||||
error: { code: "HJ-3001", message: "上游服务不可达", request_id: req.id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (upstream.body === null) {
|
||||
await writeBuffered(reply, upstream);
|
||||
return;
|
||||
}
|
||||
if (isEventStream(upstream.headers.get("content-type"))) {
|
||||
writeEventStream(req, reply, upstream);
|
||||
return;
|
||||
}
|
||||
await writeBuffered(reply, upstream);
|
||||
}
|
||||
|
||||
async function writeBuffered(reply: FastifyReply, upstream: Response): Promise<void> {
|
||||
for (const name of FORWARD_RESPONSE_HEADERS) {
|
||||
const value = upstream.headers.get(name);
|
||||
if (value != null) {
|
||||
reply.header(name, value);
|
||||
}
|
||||
}
|
||||
const payload = Buffer.from(await upstream.arrayBuffer());
|
||||
await reply.code(upstream.status).send(payload.length > 0 ? payload : null);
|
||||
}
|
||||
|
||||
function writeEventStream(req: FastifyRequest, reply: FastifyReply, upstream: Response): void {
|
||||
// 禁缓冲直通:hijack 后绕过 Fastify 序列化,逐块写底层连接
|
||||
reply.hijack();
|
||||
const raw = reply.raw;
|
||||
raw.writeHead(upstream.status, {
|
||||
"content-type": upstream.headers.get("content-type") ?? "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
"x-accel-buffering": "no",
|
||||
});
|
||||
const nodeStream = Readable.fromWeb(upstream.body as Parameters<typeof Readable.fromWeb>[0]);
|
||||
nodeStream.on("error", (err) => {
|
||||
req.log.warn({ err: String(err) }, "上游流中断");
|
||||
raw.end();
|
||||
});
|
||||
nodeStream.pipe(raw);
|
||||
}
|
||||
Reference in new issue
Block a user