Route main SSE traffic to backend

This commit is contained in:
2569718930@qq.com
2026-06-10 12:31:14 +08:00
parent afdfe03174
commit 98026e510a
4 changed files with 75 additions and 3 deletions
+13
View File
@@ -9,6 +9,19 @@ server {
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
location /api/events {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_set_header Connection '';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
@@ -49,6 +49,49 @@ function buildRuntimeContext(extra: Record<string, unknown>) {
};
}
function looksLikeHtmlDocument(value: string) {
const text = String(value || "").trim().toLowerCase();
return (
text.startsWith("<!doctype html") ||
text.startsWith("<html") ||
text.includes("<html ") ||
text.includes("cloudflare")
);
}
function extractFeedbackErrorMessage(raw: string) {
try {
const parsed = JSON.parse(String(raw || "")) as {
detail?: unknown;
error?: unknown;
message?: unknown;
};
const value = [parsed.detail, parsed.error, parsed.message].find((item) => {
return typeof item === "string" && item.trim();
});
if (typeof value !== "string") return "";
const message = value.replace(/\s+/g, " ").trim();
return looksLikeHtmlDocument(message) ? "" : message;
} catch {
return "";
}
}
function formatFeedbackSubmitError(status: number, raw: string, isEn: boolean) {
const jsonMessage = extractFeedbackErrorMessage(raw);
if (jsonMessage) return jsonMessage.slice(0, 180);
if (looksLikeHtmlDocument(raw)) {
return isEn
? `Service is temporarily unavailable. Please retry in a moment. (HTTP ${status})`
: `服务暂时不可用,请稍后重试。(HTTP ${status}`;
}
const message = String(raw || "").replace(/\s+/g, " ").trim();
if (message) return `${message.slice(0, 180)} (HTTP ${status})`;
return `HTTP ${status}`;
}
export function UserFeedbackModal({
draft,
isEn,
@@ -134,8 +177,8 @@ export function UserFeedbackModal({
}),
});
if (!res.ok) {
const detail = (await res.text().catch(() => "")).slice(0, 200);
throw new Error(detail || `HTTP ${res.status}`);
const raw = await res.text().catch(() => "");
throw new Error(formatFeedbackSubmitError(res.status, raw, isEn));
}
const payload = (await res.json().catch(() => null)) as {
feedback?: UserFeedbackEntry;
@@ -143,7 +186,8 @@ export function UserFeedbackModal({
setSubmitted(true);
onSubmitted?.(payload?.feedback);
} catch (err) {
setError(String(err).slice(0, 220));
const message = err instanceof Error ? err.message : String(err);
setError(message.slice(0, 220));
} finally {
setSubmitting(false);
}
@@ -57,6 +57,12 @@ export function runTests() {
modalSource.includes("type=\"textarea\"") === false,
"feedback modal must submit to the feedback API, lock contact to the login email when available, notify the dashboard after success, and attach client/session diagnostics without using invalid textarea input types",
);
assert(
modalSource.includes("formatFeedbackSubmitError") &&
modalSource.includes("looksLikeHtmlDocument") &&
!modalSource.includes("throw new Error(detail || `HTTP ${res.status}`)"),
"feedback modal must not expose raw Cloudflare/HTML error pages to users",
);
assert(
feedbackRouteSource.includes("export async function GET") &&
feedbackRouteSource.includes("method: \"GET\"") &&
@@ -78,6 +78,15 @@ export function runTests() {
assert(nginx.includes("location /api/events"), "Nginx deploy config must route /api/events separately");
assert(nginx.includes("proxy_buffering off"), "Nginx /api/events must disable proxy buffering for SSE");
assert(nginx.includes("proxy_read_timeout 86400s"), "Nginx /api/events must keep SSE connections open");
const frontendServerStart = nginx.indexOf("server_name polyweather.top www.polyweather.top");
const apiServerStart = nginx.indexOf("server_name api.polyweather.top");
const frontendServer = nginx.slice(frontendServerStart, apiServerStart);
assert(
frontendServer.includes("location /api/events") &&
frontendServer.includes("proxy_pass http://127.0.0.1:8000") &&
frontendServer.indexOf("location /api/events") < frontendServer.indexOf("location / {"),
"Nginx frontend host must route /api/events directly to FastAPI before the generic Next.js location",
);
const weatherSources = readRepoFile("src", "data_collection", "weather_sources.py");
assert(weatherSources.includes("_emit_temperature_patch_if_changed"), "collector must centralize temperature patch emission");