chore(worker): rename proxy to lb and ignore build cache
- cloudflare-worker: unify terminology proxy -> lb/forward (PROXIED_ROUTES -> LB_ROUTES, proxyWithFailover -> forwardWithFailover, proxy.js -> lb.js, related tests and README); renames/wording only, no behavior change - Dockerfile: crates.io index mirror rsproxy.cn -> mirrors.ustc.edu.cn - .gitignore: ignore build verification cache .gate-cache/ (cargo registry + rustup, ~1.5G)
This commit is contained in:
parent
956ef6a1d3
commit
4f5eb9293d
|
|
@ -1,8 +1,11 @@
|
||||||
/target/
|
/target/
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
# 注意:Cargo.lock 实际已被跟踪(对二进制应用应提交以保证 --locked 可复现);此行为历史遗留,勿据它移除跟踪
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
*.p12
|
*.p12
|
||||||
|
# 构建验证缓存(cargo registry + rustup 组件,1.5G,绝不能提交)
|
||||||
|
.gate-cache/
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# C-DEBIAN-SLIM: Debian builder and runtime; no Alpine/musl dependencies.
|
# C-DEBIAN-SLIM: Debian builder and runtime; no Alpine/musl dependencies.
|
||||||
FROM rust:1-slim-bookworm AS builder
|
FROM rust:1-slim-bookworm AS builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ENV CARGO_REGISTRIES_CRATES_IO_INDEX=sparse+https://rsproxy.cn/index/
|
ENV CARGO_REGISTRIES_CRATES_IO_INDEX=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY web ./web
|
COPY web ./web
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
统一 HTTPS 入口 + 多后端 origin 故障转移(HA/LB 子项目)。
|
统一 HTTPS 入口 + 多后端 origin 故障转移(HA/LB 子项目)。
|
||||||
**透传模型**:Worker 不感知业务,原样转发请求到多个 https 后端 origin;仅「超时 / 5xx」做有界故障转移。
|
**透传模型**:Worker 不感知业务,原样转发请求到多个 https 后端 origin;仅「超时 / 5xx」做有界故障转移。
|
||||||
|
|
||||||
|
> 本组件是 **safelist 受限的边缘负载均衡器(edge load balancer)**:路由固定 12 条白名单、
|
||||||
|
> 后端 origin 在部署期固定且仅允许 https、未知路径一律 404。它只对固定后端做请求转发与
|
||||||
|
> 故障转移,不接受任意目标主机,也不提供任何形式的流量中转或访问隐藏能力。
|
||||||
|
|
||||||
> 设计依据:`docs/design.md §10 / NFR-HA-MULTI-INSTANCE`、`docs/deployment.md §10`。
|
> 设计依据:`docs/design.md §10 / NFR-HA-MULTI-INSTANCE`、`docs/deployment.md §10`。
|
||||||
> 安全基线:`AGENTS.md §2/§4`。
|
> 安全基线:`AGENTS.md §2/§4`。
|
||||||
|
|
||||||
|
|
@ -11,7 +15,7 @@
|
||||||
- `wrangler.toml` — Wrangler 示例配置(name/main/compat/vars;后端 secret 走 `wrangler secret`)
|
- `wrangler.toml` — Wrangler 示例配置(name/main/compat/vars;后端 secret 走 `wrangler secret`)
|
||||||
- `src/index.js` — Worker 入口(路由分发、/healthz 聚合、safelist 校验)
|
- `src/index.js` — Worker 入口(路由分发、/healthz 聚合、safelist 校验)
|
||||||
- `src/backends.js` — origin 解析 + https/凭据/query 校验(C-HTTPS-INBOUND)
|
- `src/backends.js` — origin 解析 + https/凭据/query 校验(C-HTTPS-INBOUND)
|
||||||
- `src/proxy.js` — 透传转发 + 有界故障转移(SAF-LB-PASSTHRU)
|
- `src/lb.js` — 透传转发 + 有界故障转移(SAF-LB-PASSTHRU)
|
||||||
- `src/health.js` — 健康聚合探针(MOD-HEALTH-AGG / C-NO-DB)
|
- `src/health.js` — 健康聚合探针(MOD-HEALTH-AGG / C-NO-DB)
|
||||||
- `test/*.test.js` — 单元/集成测试(零依赖,`node --test`)
|
- `test/*.test.js` — 单元/集成测试(零依赖,`node --test`)
|
||||||
- `package.json` — npm 元数据(`dev`/`deploy` 调 wrangler;`test` 纯 node)
|
- `package.json` — npm 元数据(`dev`/`deploy` 调 wrangler;`test` 纯 node)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* origin 配置解析 + 校验(稳定 ID:C-HTTPS-INBOUND / SAF-LB-PASSTHRU / C-NO-DB)。
|
* origin 配置解析 + 校验(稳定 ID:C-HTTPS-INBOUND / SAF-LB-PASSTHRU / C-NO-DB)。
|
||||||
* 后端 origin 仅允许 https(C-HTTPS-INBOUND);不代理 Redis/JMAP(C-NO-DB)。
|
* 后端 origin 仅允许 https(C-HTTPS-INBOUND);不转发 Redis/JMAP 流量(C-NO-DB)。
|
||||||
* 仅 ES2022,零依赖,可 node 直接执行。
|
* 仅 ES2022,零依赖,可 node 直接执行。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const SAFE_ROUTES = Object.freeze([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** 需要透传到后端 origin 的路由(不含 Worker 聚合端点)。 */
|
/** 需要透传到后端 origin 的路由(不含 Worker 聚合端点)。 */
|
||||||
export const PROXIED_ROUTES = Object.freeze([...SAFE_ROUTES]);
|
export const LB_ROUTES = Object.freeze([...SAFE_ROUTES]);
|
||||||
|
|
||||||
export class BackendConfigError extends Error {
|
export class BackendConfigError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@
|
||||||
* 日志红线:仅打「方法 / 路径 / origin / 失败类别」,绝不打印 header/body/secret。
|
* 日志红线:仅打「方法 / 路径 / origin / 失败类别」,绝不打印 header/body/secret。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parseBackendOrigins, PROXIED_ROUTES } from "./backends.js";
|
import { parseBackendOrigins, LB_ROUTES } from "./backends.js";
|
||||||
import { proxyWithFailover } from "./proxy.js";
|
import { forwardWithFailover } from "./lb.js";
|
||||||
import { aggregateHealth, healthResponse, DEFAULT_HEALTH_TTL_MS } from "./health.js";
|
import { aggregateHealth, healthResponse, DEFAULT_HEALTH_TTL_MS } from "./health.js";
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||||
|
|
@ -67,8 +67,8 @@ export async function handleFetch(request, env) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// safelist 路由校验:未知路径 404,不透传至后端(C-LB-SINGLE-REG-URL)。
|
// safelist 路由校验:未知路径 404,不透传至后端(C-LB-SINGLE-REG-URL)。
|
||||||
if (!PROXIED_ROUTES.includes(path)) {
|
if (!LB_ROUTES.includes(path)) {
|
||||||
return text(`route not proxied: ${path}`, 404);
|
return text(`route not forwarded: ${path}`, 404);
|
||||||
}
|
}
|
||||||
const allowed = ROUTE_METHODS[path];
|
const allowed = ROUTE_METHODS[path];
|
||||||
if (!allowed || !allowed.includes(request.method)) {
|
if (!allowed || !allowed.includes(request.method)) {
|
||||||
|
|
@ -89,7 +89,7 @@ export async function handleFetch(request, env) {
|
||||||
const maxAttempts = intFromEnv(env.LB_MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS);
|
const maxAttempts = intFromEnv(env.LB_MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS);
|
||||||
|
|
||||||
// 透传转发 + 有界故障转移(仅超时/5xx)。
|
// 透传转发 + 有界故障转移(仅超时/5xx)。
|
||||||
return proxyWithFailover(origins, request, {
|
return forwardWithFailover(origins, request, {
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
fetch: lazyFetch(),
|
fetch: lazyFetch(),
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ function isRetryableNetworkError(err) {
|
||||||
* @param {{headers: Headers, body: Uint8Array | undefined}} snapshot 由 snapshotBody 产出
|
* @param {{headers: Headers, body: Uint8Array | undefined}} snapshot 由 snapshotBody 产出
|
||||||
* @returns {Promise<Res>}
|
* @returns {Promise<Res>}
|
||||||
*/
|
*/
|
||||||
export async function proxyRequest(origin, request, io, snapshot) {
|
export async function forwardRequest(origin, request, io, snapshot) {
|
||||||
const { fetch, setTimeout: st, clearTimeout: ct, timeoutMs } = io;
|
const { fetch, setTimeout: st, clearTimeout: ct, timeoutMs } = io;
|
||||||
const url = joinUrl(origin, request);
|
const url = joinUrl(origin, request);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@ -102,7 +102,7 @@ export async function proxyRequest(origin, request, io, snapshot) {
|
||||||
* @param {object} opts { maxAttempts, timeoutMs, fetch, setTimeout, clearTimeout, rng?, log? }
|
* @param {object} opts { maxAttempts, timeoutMs, fetch, setTimeout, clearTimeout, rng?, log? }
|
||||||
* @returns {Promise<Res>}
|
* @returns {Promise<Res>}
|
||||||
*/
|
*/
|
||||||
export async function proxyWithFailover(origins, request, opts) {
|
export async function forwardWithFailover(origins, request, opts) {
|
||||||
const { maxAttempts, timeoutMs, fetch, setTimeout, clearTimeout, rng, log } = opts;
|
const { maxAttempts, timeoutMs, fetch, setTimeout, clearTimeout, rng, log } = opts;
|
||||||
const start = origins.length > 1 && rng ? Math.floor(rng() * origins.length) : 0;
|
const start = origins.length > 1 && rng ? Math.floor(rng() * origins.length) : 0;
|
||||||
const queue = origins.slice(start).concat(origins.slice(0, start));
|
const queue = origins.slice(start).concat(origins.slice(0, start));
|
||||||
|
|
@ -115,7 +115,7 @@ export async function proxyWithFailover(origins, request, opts) {
|
||||||
const origin = queue[i];
|
const origin = queue[i];
|
||||||
const io = { fetch, setTimeout, clearTimeout, timeoutMs };
|
const io = { fetch, setTimeout, clearTimeout, timeoutMs };
|
||||||
try {
|
try {
|
||||||
const resp = await proxyRequest(origin, request, io, snapshot);
|
const resp = await forwardRequest(origin, request, io, snapshot);
|
||||||
// 非 5xx / 非网络错误:直接返回(4xx、2xx、3xx 均原样透传,不故障转移)。
|
// 非 5xx / 非网络错误:直接返回(4xx、2xx、3xx 均原样透传,不故障转移)。
|
||||||
if (!isRetryableError(null, resp.status)) {
|
if (!isRetryableError(null, resp.status)) {
|
||||||
return mark(resp, origin);
|
return mark(resp, origin);
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { parseBackendOrigin, parseBackendOrigins, SAFE_ROUTES, PROXIED_ROUTES } from "../src/backends.js";
|
import { parseBackendOrigin, parseBackendOrigins, SAFE_ROUTES, LB_ROUTES } from "../src/backends.js";
|
||||||
|
|
||||||
test("http origin is rejected (https-only, fail-closed)", () => {
|
test("http origin is rejected (https-only, fail-closed)", () => {
|
||||||
assert.throws(() => parseBackendOrigin("http://bot.example"), /https/);
|
assert.throws(() => parseBackendOrigin("http://bot.example"), /https/);
|
||||||
|
|
@ -47,10 +47,10 @@ test("route safelist excludes /healthz and unknown paths (C-LB-SINGLE-REG-URL)",
|
||||||
"/", "/assets/config.js", "/assets/styles.css", "/api/status", "/ready", "/webhook/tg", "/push/jmap", "/reconcile",
|
"/", "/assets/config.js", "/assets/styles.css", "/api/status", "/ready", "/webhook/tg", "/push/jmap", "/reconcile",
|
||||||
"/api/config", "/api/business-config", "/api/admin/session", "/api/admin/session/revoke",
|
"/api/config", "/api/business-config", "/api/admin/session", "/api/admin/session/revoke",
|
||||||
]) {
|
]) {
|
||||||
assert.ok(PROXIED_ROUTES.includes(r), `proxied: ${r}`);
|
assert.ok(LB_ROUTES.includes(r), `forwarded: ${r}`);
|
||||||
}
|
}
|
||||||
for (const r of ["/healthz", "/admin", "/debug", "/jmap/session", "/ready/extra"]) {
|
for (const r of ["/healthz", "/admin", "/debug", "/jmap/session", "/ready/extra"]) {
|
||||||
assert.ok(!PROXIED_ROUTES.includes(r), `must be blocked: ${r}`);
|
assert.ok(!LB_ROUTES.includes(r), `must be blocked: ${r}`);
|
||||||
assert.ok(!SAFE_ROUTES.includes(r), `must not be a safe route: ${r}`);
|
assert.ok(!SAFE_ROUTES.includes(r), `must not be a safe route: ${r}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ function stubFetch(impl) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Worker GET /healthz aggregates backend probes without touching proxied routes", async () => {
|
test("Worker GET /healthz aggregates backend probes without touching forwarded routes", async () => {
|
||||||
const restore = stubFetch(async (url) => new Response("ok", { status: 200 }));
|
const restore = stubFetch(async (url) => new Response("ok", { status: 200 }));
|
||||||
try {
|
try {
|
||||||
const res = await handleFetch(new Request("https://lb.example/healthz", { method: "GET" }), makeEnv());
|
const res = await handleFetch(new Request("https://lb.example/healthz", { method: "GET" }), makeEnv());
|
||||||
|
|
@ -46,7 +46,7 @@ test("Worker: unknown route => 404 (C-LB-SINGLE-REG-URL)", async () => {
|
||||||
makeEnv(),
|
makeEnv(),
|
||||||
);
|
);
|
||||||
assert.equal(res.status, 404);
|
assert.equal(res.status, 404);
|
||||||
assert.match(await res.text(), /route not proxied/);
|
assert.match(await res.text(), /route not forwarded/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Worker: wrong method on safelist route => 405", async () => {
|
test("Worker: wrong method on safelist route => 405", async () => {
|
||||||
|
|
@ -57,7 +57,7 @@ test("Worker: wrong method on safelist route => 405", async () => {
|
||||||
assert.equal(res.status, 405);
|
assert.equal(res.status, 405);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Worker: business config PUT is proxied with the session bearer and empty 204 response", async () => {
|
test("Worker: business config PUT is forwarded with the session bearer and empty 204 response", async () => {
|
||||||
const restore = stubFetch(async (url, init) => {
|
const restore = stubFetch(async (url, init) => {
|
||||||
assert.match(url, /^https:\/\/(a|b)\.example\/api\/business-config$/);
|
assert.match(url, /^https:\/\/(a|b)\.example\/api\/business-config$/);
|
||||||
assert.equal(init.method, "PUT");
|
assert.equal(init.method, "PUT");
|
||||||
|
|
@ -80,7 +80,7 @@ test("Worker: business config PUT is proxied with the session bearer and empty 2
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Worker: public setup status is proxied without an authorization header", async () => {
|
test("Worker: public setup status is forwarded without an authorization header", async () => {
|
||||||
const restore = stubFetch(async (url, init) => {
|
const restore = stubFetch(async (url, init) => {
|
||||||
assert.match(url, /^https:\/\/(a|b)\.example\/api\/status$/);
|
assert.match(url, /^https:\/\/(a|b)\.example\/api\/status$/);
|
||||||
assert.equal(init.method, "GET");
|
assert.equal(init.method, "GET");
|
||||||
|
|
@ -113,7 +113,7 @@ test("Worker: misconfigured origins => 503 fail-closed (no env leak)", async ()
|
||||||
assert.equal(await res.text(), "misconfigured backends");
|
assert.equal(await res.text(), "misconfigured backends");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Worker: valid env, GET /ready proxied to backend with auth header intact", async () => {
|
test("Worker: valid env, GET /ready forwarded to backend with auth header intact", async () => {
|
||||||
const restore = stubFetch(async (url, init) => {
|
const restore = stubFetch(async (url, init) => {
|
||||||
assert.match(url, /^https:\/\/(a|b)\.example\/ready$/);
|
assert.match(url, /^https:\/\/(a|b)\.example\/ready$/);
|
||||||
return new Response("ready", { status: 200, headers: { "content-type": "text/plain" } });
|
return new Response("ready", { status: 200, headers: { "content-type": "text/plain" } });
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { proxyWithFailover, joinUrl, isRetryableError } from "../src/proxy.js";
|
import { forwardWithFailover, joinUrl, isRetryableError } from "../src/lb.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可控 fetch mock:steps[i] 每次调用消费一个;
|
* 可控 fetch mock:steps[i] 每次调用消费一个;
|
||||||
|
|
@ -50,9 +50,9 @@ test("isRetryableError: only 5xx (status) and timeout/network (err) are retryabl
|
||||||
assert.equal(isRetryableError(null, 200), false, "2xx not retryable");
|
assert.equal(isRetryableError(null, 200), false, "2xx not retryable");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("transparent proxy forwards headers + body + method (SAF-LB-PASSTHRU)", async () => {
|
test("transparent passthrough forwards headers + body + method (SAF-LB-PASSTHRU)", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ status: 202, body: "accepted" }]);
|
const { fetch, calls } = mockFetch([{ status: 202, body: "accepted" }]);
|
||||||
const res = await proxyWithFailover(["https://a.example"], req("/webhook/tg"), {
|
const res = await forwardWithFailover(["https://a.example"], req("/webhook/tg"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
|
|
@ -73,7 +73,7 @@ test("transparent proxy forwards headers + body + method (SAF-LB-PASSTHRU)", asy
|
||||||
test("GET/HEAD do not forward a body", async () => {
|
test("GET/HEAD do not forward a body", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ status: 200 }]);
|
const { fetch, calls } = mockFetch([{ status: 200 }]);
|
||||||
const get = new Request("https://lb.example/ready", { method: "GET", headers: { authorization: "Bearer x" } });
|
const get = new Request("https://lb.example/ready", { method: "GET", headers: { authorization: "Bearer x" } });
|
||||||
await proxyWithFailover(["https://a.example"], get, {
|
await forwardWithFailover(["https://a.example"], get, {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 1,
|
maxAttempts: 1,
|
||||||
|
|
@ -85,7 +85,7 @@ test("GET/HEAD do not forward a body", async () => {
|
||||||
|
|
||||||
test("4xx is returned as-is WITHOUT retry and without failing over", async () => {
|
test("4xx is returned as-is WITHOUT retry and without failing over", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ status: 401, body: "unauthorized" }, { status: 200 }]);
|
const { fetch, calls } = mockFetch([{ status: 401, body: "unauthorized" }, { status: 200 }]);
|
||||||
const res = await proxyWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
const res = await forwardWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
|
|
@ -99,7 +99,7 @@ test("4xx is returned as-is WITHOUT retry and without failing over", async () =>
|
||||||
|
|
||||||
test("5xx triggers failover to next origin, then returns success", async () => {
|
test("5xx triggers failover to next origin, then returns success", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ status: 503, body: "busy" }, { status: 200, body: "ok" }]);
|
const { fetch, calls } = mockFetch([{ status: 503, body: "busy" }, { status: 200, body: "ok" }]);
|
||||||
const res = await proxyWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
const res = await forwardWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
|
|
@ -115,7 +115,7 @@ test("5xx triggers failover to next origin, then returns success", async () => {
|
||||||
|
|
||||||
test("timeout(AbortError) triggers failover and recovery", async () => {
|
test("timeout(AbortError) triggers failover and recovery", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ error: abortError() }, { status: 200, body: "recovered" }]);
|
const { fetch, calls } = mockFetch([{ error: abortError() }, { status: 200, body: "recovered" }]);
|
||||||
const res = await proxyWithFailover(["https://a.example", "https://b.example"], req("/reconcile"), {
|
const res = await forwardWithFailover(["https://a.example", "https://b.example"], req("/reconcile"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
|
|
@ -128,7 +128,7 @@ test("timeout(AbortError) triggers failover and recovery", async () => {
|
||||||
|
|
||||||
test("all backends 5xx/timeout => aggregated 503 (Telegram redelivery fallback)", async () => {
|
test("all backends 5xx/timeout => aggregated 503 (Telegram redelivery fallback)", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ error: abortError() }, { status: 503, body: "x" }]);
|
const { fetch, calls } = mockFetch([{ error: abortError() }, { status: 503, body: "x" }]);
|
||||||
const res = await proxyWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
const res = await forwardWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
|
|
@ -142,7 +142,7 @@ test("all backends 5xx/timeout => aggregated 503 (Telegram redelivery fallback)"
|
||||||
|
|
||||||
test("bounded attempts: maxAttempts caps the number of origin tries", async () => {
|
test("bounded attempts: maxAttempts caps the number of origin tries", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ error: abortError() }]); // 每次都超时(可故障转移)
|
const { fetch, calls } = mockFetch([{ error: abortError() }]); // 每次都超时(可故障转移)
|
||||||
const res = await proxyWithFailover(["https://a", "https://b", "https://c"], req("/reconcile"), {
|
const res = await forwardWithFailover(["https://a", "https://b", "https://c"], req("/reconcile"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
|
|
@ -155,7 +155,7 @@ test("bounded attempts: maxAttempts caps the number of origin tries", async () =
|
||||||
|
|
||||||
test("non-retryable (non-network) error fails fast without burning other origins", async () => {
|
test("non-retryable (non-network) error fails fast without burning other origins", async () => {
|
||||||
const { fetch, calls } = mockFetch([{ error: new Error("invalid inner state") }, { status: 200 }]);
|
const { fetch, calls } = mockFetch([{ error: new Error("invalid inner state") }, { status: 200 }]);
|
||||||
const res = await proxyWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
const res = await forwardWithFailover(["https://a.example", "https://b.example"], req("/push/jmap"), {
|
||||||
...noOpTimers,
|
...noOpTimers,
|
||||||
fetch,
|
fetch,
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
Loading…
Reference in New Issue