Proxy routes now share one upstream-error adapter so client-actionable statuses such as auth, entitlement, validation, and rate limits survive the BFF instead of becoming opaque 502s. The scan terminal mobile overrides also load last and remove desktop rail constraints so phones get a single readable column. Constraint: Mobile users reported the dashboard was unreadable, and BFF proxy errors were masking expected client states. Rejected: Let every route keep bespoke error JSON | continued inconsistent status codes and production detail leakage. Confidence: high Scope-risk: moderate Directive: Keep ScanTerminalMobile.module.css imported after desktop scan-terminal CSS so mobile breakpoints win. Tested: npx tsc --noEmit --pretty false --project frontend/tsconfig.json Tested: npm run build Tested: npm run test:business Tested: Chrome mobile smoke test at 390px with no horizontal overflow Not-tested: Real device Safari/Android manual QA
68 lines
1.9 KiB
TypeScript
68 lines
1.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import {
|
|
applyAuthResponseCookies,
|
|
buildBackendRequestHeaders,
|
|
} from "@/lib/backend-auth";
|
|
import {
|
|
buildProxyExceptionResponse,
|
|
buildUpstreamErrorResponse,
|
|
} from "@/lib/api-proxy";
|
|
|
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
|
|
|
export const dynamic = "force-dynamic";
|
|
export const maxDuration = 90;
|
|
|
|
export async function POST(req: NextRequest) {
|
|
if (!API_BASE) {
|
|
return NextResponse.json(
|
|
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
let body: unknown = {};
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
body = {};
|
|
}
|
|
|
|
const requestBody =
|
|
body && typeof body === "object" ? (body as Record<string, unknown>) : {};
|
|
const auth = await buildBackendRequestHeaders(req);
|
|
const headers = new Headers(auth.headers);
|
|
headers.set("Content-Type", "application/json");
|
|
headers.set("Accept", "text/event-stream");
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/scan/terminal/ai-city/stream`, {
|
|
method: "POST",
|
|
headers,
|
|
cache: "no-store",
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
if (!res.ok || !res.body) {
|
|
const raw = await res.text();
|
|
const response = buildUpstreamErrorResponse(res.status, raw);
|
|
return applyAuthResponseCookies(response, auth.response);
|
|
}
|
|
|
|
const response = new NextResponse(res.body, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
"Cache-Control": "no-store, no-transform",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
});
|
|
return applyAuthResponseCookies(response, auth.response);
|
|
} catch (error) {
|
|
const response = buildProxyExceptionResponse(error, {
|
|
publicMessage: "Failed to stream city AI data",
|
|
extra: { city: requestBody.city },
|
|
});
|
|
return applyAuthResponseCookies(response, auth.response);
|
|
}
|
|
}
|