fix: prevent slow chart proxy 504s

This commit is contained in:
2569718930@qq.com
2026-06-03 02:34:27 +08:00
parent 92307a9f97
commit 00f96150c5
5 changed files with 91 additions and 11 deletions
@@ -11,6 +11,38 @@ const DETAIL_BATCH_PROXY_TIMEOUT_MS = Number(
process.env.POLYWEATHER_CITY_DETAIL_BATCH_PROXY_TIMEOUT_MS || "15000",
);
function parseRequestedCities(req: NextRequest) {
const requestedLimit = Number(req.nextUrl.searchParams.get("limit") || "12");
const limit = Number.isFinite(requestedLimit)
? Math.max(1, Math.min(24, requestedLimit))
: 12;
const seen = new Set<string>();
const requestedCities: string[] = [];
for (const item of (req.nextUrl.searchParams.get("cities") || "").split(
",",
)) {
const city = item.trim();
if (!city || seen.has(city)) continue;
seen.add(city);
requestedCities.push(city);
if (requestedCities.length >= limit) break;
}
return requestedCities;
}
function buildCityDetailBatchTimeoutPayload(requestedCities: string[]) {
return {
cities: requestedCities,
details: {},
errors: {},
missing: requestedCities,
partial: true,
timeout: true,
};
}
export async function GET(req: NextRequest) {
const timer = createProxyTimer(req, "city_detail_batch");
if (!API_BASE) {
@@ -25,6 +57,7 @@ export async function GET(req: NextRequest) {
}
const forceRefresh = req.nextUrl.searchParams.get("force_refresh") ?? "false";
const requestedCities = parseRequestedCities(req);
const cachePolicy = buildCityDetailProxyCachePolicy(forceRefresh, 15);
const searchParams = new URLSearchParams({
cities: req.nextUrl.searchParams.get("cities") || "",
@@ -52,6 +85,11 @@ export async function GET(req: NextRequest) {
publicMessage: "Failed to fetch city detail batch",
revalidateSeconds: cachePolicy.revalidateSeconds,
signal: controller.signal,
timeoutResponse: () =>
NextResponse.json(buildCityDetailBatchTimeoutPayload(requestedCities), {
headers: { "Cache-Control": "no-store, max-age=0" },
status: 200,
}),
timeoutPublicMessage: "City detail batch request timed out",
timing: timer,
url: `${API_BASE}/api/cities/detail-batch?${searchParams.toString()}`,
+1 -1
View File
@@ -9,7 +9,7 @@ import {
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "18000",
process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "35000",
);
export const maxDuration = 45;
@@ -56,6 +56,26 @@ export function runTests() {
/cacheControlForData/,
"city detail batch proxy should be able to suppress response caching for partial payloads",
);
assert.match(
detailBatchProxy,
/buildCityDetailBatchTimeoutPayload/,
"city detail batch proxy should degrade proxy timeouts into the same partial payload shape the chart client already tolerates",
);
assert.match(
detailBatchProxy,
/missing:\s*requestedCities/,
"city detail batch proxy timeout fallback should mark requested cities as missing instead of surfacing a browser 504",
);
assert.match(
detailBatchProxy,
/partial:\s*true/,
"city detail batch proxy timeout fallback should preserve partial response semantics",
);
assert.match(
detailBatchProxy,
/status:\s*200/,
"city detail batch proxy timeout fallback should avoid red 504 fetch failures for optional chart enrichment",
);
assert.match(
apiProxySource,
/cacheControlForData\?:/,
@@ -65,6 +85,16 @@ export function runTests() {
const scanTerminalProxy = readFrontend("app", "api", "scan", "terminal", "route.ts");
assert.match(scanTerminalProxy, /createProxyTimer\(req,\s*"scan_terminal"\)/);
assert.match(scanTerminalProxy, /timing:\s*timer/);
assert.match(
scanTerminalProxy,
/POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS\s*\|\|\s*"35000"/,
"scan terminal proxy should allow the production backend enough time to return before the 45 second route cap",
);
assert.match(
scanTerminalProxy,
/export const maxDuration = 45/,
"scan terminal proxy timeout budget should remain below the Next route execution cap",
);
const cityDetailProxy = readFrontend("app", "api", "city", "[name]", "detail", "route.ts");
assert.match(cityDetailProxy, /createProxyTimer\(req,\s*"city_detail"\)/);
+21 -9
View File
@@ -124,6 +124,7 @@ export async function proxyBackendJsonGet(
signal?: AbortSignal;
statusOnException?: number;
timeoutPublicMessage?: string;
timeoutResponse?: () => NextResponse;
timing?: ProxyTimer;
url: string;
},
@@ -195,16 +196,27 @@ export async function proxyBackendJsonGet(
: withCookies;
} catch (error) {
const timedOut = options.signal?.aborted === true;
const response = buildProxyExceptionResponse(error, {
publicMessage:
timedOut && options.timeoutPublicMessage
? options.timeoutPublicMessage
: options.publicMessage,
status: timedOut ? 504 : options.statusOnException,
});
const withCookies = auth ? applyAuthResponseCookies(response, auth.response) : response;
const response =
timedOut && options.timeoutResponse
? options.timeoutResponse()
: buildProxyExceptionResponse(error, {
publicMessage:
timedOut && options.timeoutPublicMessage
? options.timeoutPublicMessage
: options.publicMessage,
status: timedOut ? 504 : options.statusOnException,
});
const withCookies = auth
? applyAuthResponseCookies(response, auth.response)
: response;
const outcome =
timedOut && options.timeoutResponse
? "timeout_fallback"
: timedOut
? "timeout"
: "exception";
return timing
? finishProxyTimedResponse(withCookies, timing, timedOut ? "timeout" : "exception")
? finishProxyTimedResponse(withCookies, timing, outcome)
: withCookies;
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ def test_scan_terminal_backend_timeout_returns_before_next_proxy_abort():
ROOT / "web" / "services" / "scan_terminal_config.py"
).read_text(encoding="utf-8")
assert 'POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "18000"' in route_source
assert 'POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "35000"' in route_source
assert '"POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC",\n 10,' in config_source
assert (
'"POLYWEATHER_SCAN_TERMINAL_PREWARM_PAYLOAD_TIMEOUT_SEC",\n 30,'