Make weather-market UI reliable before shipping
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
This commit is contained in:
@@ -3,6 +3,10 @@ import {
|
||||
applyAuthResponseCookies,
|
||||
buildBackendRequestHeaders,
|
||||
} from "@/lib/backend-auth";
|
||||
import {
|
||||
buildProxyExceptionResponse,
|
||||
buildUpstreamErrorResponse,
|
||||
} from "@/lib/api-proxy";
|
||||
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
const AI_CITY_GATEWAY_TIMEOUT_MS = Math.max(
|
||||
@@ -63,10 +67,7 @@ export async function POST(req: NextRequest) {
|
||||
status: res.status,
|
||||
detail: raw.slice(0, 180),
|
||||
});
|
||||
const response = NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
||||
{ status: res.status === 402 || res.status === 403 ? res.status : 502 },
|
||||
);
|
||||
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
}
|
||||
const data = await res.json();
|
||||
@@ -93,18 +94,17 @@ export async function POST(req: NextRequest) {
|
||||
timeout_ms: AI_CITY_GATEWAY_TIMEOUT_MS,
|
||||
error: String(error),
|
||||
});
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
error: timedOut
|
||||
? "City AI gateway timed out before backend responded"
|
||||
: "Failed to fetch city AI data",
|
||||
detail: String(error),
|
||||
const response = buildProxyExceptionResponse(error, {
|
||||
publicMessage: timedOut
|
||||
? "City AI gateway timed out before backend responded"
|
||||
: "Failed to fetch city AI data",
|
||||
status: timedOut ? 504 : 500,
|
||||
extra: {
|
||||
elapsed_ms: elapsedMs,
|
||||
timeout_ms: AI_CITY_GATEWAY_TIMEOUT_MS,
|
||||
city: requestBody.city,
|
||||
},
|
||||
{ status: timedOut ? 504 : 500 },
|
||||
);
|
||||
});
|
||||
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
applyAuthResponseCookies,
|
||||
buildBackendRequestHeaders,
|
||||
} from "@/lib/backend-auth";
|
||||
import {
|
||||
buildProxyExceptionResponse,
|
||||
buildUpstreamErrorResponse,
|
||||
} from "@/lib/api-proxy";
|
||||
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
|
||||
@@ -40,10 +44,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
if (!res.ok || !res.body) {
|
||||
const raw = await res.text();
|
||||
const response = NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
||||
{ status: res.status === 402 || res.status === 403 ? res.status : 502 },
|
||||
);
|
||||
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
}
|
||||
|
||||
@@ -57,14 +58,10 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
} catch (error) {
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
error: "Failed to stream city AI data",
|
||||
detail: String(error),
|
||||
city: requestBody.city,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
const response = buildProxyExceptionResponse(error, {
|
||||
publicMessage: "Failed to stream city AI data",
|
||||
extra: { city: requestBody.city },
|
||||
});
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
applyAuthResponseCookies,
|
||||
buildBackendRequestHeaders,
|
||||
} from "@/lib/backend-auth";
|
||||
import {
|
||||
buildProxyExceptionResponse,
|
||||
buildUpstreamErrorResponse,
|
||||
} from "@/lib/api-proxy";
|
||||
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
const SCAN_AI_PROXY_TIMEOUT_MS = Math.max(
|
||||
@@ -46,10 +50,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const raw = await res.text();
|
||||
const response = NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
||||
{ status: res.status === 402 || res.status === 403 ? res.status : 502 },
|
||||
);
|
||||
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
}
|
||||
const data = await res.json();
|
||||
@@ -61,15 +62,12 @@ export async function POST(req: NextRequest) {
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
} catch (error) {
|
||||
const timedOut = controller.signal.aborted;
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
error: timedOut
|
||||
? "Scan AI request timed out"
|
||||
: "Failed to fetch scan AI data",
|
||||
detail: String(error),
|
||||
},
|
||||
{ status: timedOut ? 504 : 500 },
|
||||
);
|
||||
const response = buildProxyExceptionResponse(error, {
|
||||
publicMessage: timedOut
|
||||
? "Scan AI request timed out"
|
||||
: "Failed to fetch scan AI data",
|
||||
status: timedOut ? 504 : 500,
|
||||
});
|
||||
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
applyAuthResponseCookies,
|
||||
buildBackendRequestHeaders,
|
||||
} from "@/lib/backend-auth";
|
||||
import {
|
||||
buildProxyExceptionResponse,
|
||||
buildUpstreamErrorResponse,
|
||||
} from "@/lib/api-proxy";
|
||||
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
|
||||
@@ -54,10 +58,7 @@ export async function GET(req: NextRequest) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const raw = await res.text();
|
||||
const response = NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) },
|
||||
{ status: 502 },
|
||||
);
|
||||
const response = buildUpstreamErrorResponse(res.status, raw);
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
}
|
||||
const data = await res.json();
|
||||
@@ -69,15 +70,12 @@ export async function GET(req: NextRequest) {
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
} catch (error) {
|
||||
const timedOut = controller.signal.aborted;
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
error: timedOut
|
||||
? "Scan terminal request timed out"
|
||||
: "Failed to fetch scan terminal data",
|
||||
detail: String(error),
|
||||
},
|
||||
{ status: timedOut ? 504 : 500 },
|
||||
);
|
||||
const response = buildProxyExceptionResponse(error, {
|
||||
publicMessage: timedOut
|
||||
? "Scan terminal request timed out"
|
||||
: "Failed to fetch scan terminal data",
|
||||
status: timedOut ? 504 : 500,
|
||||
});
|
||||
return auth ? applyAuthResponseCookies(response, auth.response) : response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
Reference in New Issue
Block a user