diff --git a/frontend/app/api/analytics/events/route.ts b/frontend/app/api/analytics/events/route.ts index 4a895a2a..01517a3c 100644 --- a/frontend/app/api/analytics/events/route.ts +++ b/frontend/app/api/analytics/events/route.ts @@ -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 ANALYTICS_ENABLED = @@ -33,19 +37,17 @@ 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, 260) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 260, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to track analytics event", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to track analytics event", + }); } } diff --git a/frontend/app/api/auth/me/route.ts b/frontend/app/api/auth/me/route.ts index d300002e..4cefedf2 100644 --- a/frontend/app/api/auth/me/route.ts +++ b/frontend/app/api/auth/me/route.ts @@ -3,6 +3,10 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { + buildProxyExceptionResponse, + buildUpstreamErrorResponse, +} from "@/lib/api-proxy"; import { getLocalDevAuthPayload, isLocalFullAccessHost, @@ -70,10 +74,7 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } - const response = NextResponse.json( - { error: `Backend returned ${res.status}`, detail: raw.slice(0, 300) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); @@ -98,10 +99,9 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } - return NextResponse.json( - { error: "Failed to fetch auth profile", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch auth profile", + }); } } diff --git a/frontend/app/api/cities/route.ts b/frontend/app/api/cities/route.ts index 1c73c84a..8a9d2267 100644 --- a/frontend/app/api/cities/route.ts +++ b/frontend/app/api/cities/route.ts @@ -3,6 +3,10 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { + buildProxyExceptionResponse, + buildUpstreamErrorResponse, +} from "@/lib/api-proxy"; import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -27,10 +31,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(); @@ -41,10 +42,9 @@ export async function GET(req: NextRequest) { ); return applyAuthResponseCookies(response, auth.response); } catch (error) { - const response = NextResponse.json( - { error: "Failed to fetch cities", detail: String(error) }, - { status: 500 }, - ); + const response = buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch cities", + }); return response; } } diff --git a/frontend/app/api/city/[name]/detail/route.ts b/frontend/app/api/city/[name]/detail/route.ts index 745a6437..f8d40621 100644 --- a/frontend/app/api/city/[name]/detail/route.ts +++ b/frontend/app/api/city/[name]/detail/route.ts @@ -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; @@ -41,20 +45,16 @@ export async function GET( }); 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(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - const response = NextResponse.json( - { error: "Failed to fetch city detail aggregate", detail: String(error) }, - { status: 500 }, - ); + const response = buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch city detail aggregate", + }); return response; } } diff --git a/frontend/app/api/city/[name]/market-scan/route.ts b/frontend/app/api/city/[name]/market-scan/route.ts index 165e419b..78b96827 100644 --- a/frontend/app/api/city/[name]/market-scan/route.ts +++ b/frontend/app/api/city/[name]/market-scan/route.ts @@ -3,17 +3,13 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { + buildProxyExceptionResponse, + buildUpstreamErrorResponse, +} from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; -function parseBackendError(raw: string) { - try { - return JSON.parse(raw) as unknown; - } catch { - return raw.slice(0, 800); - } -} - export async function GET( req: NextRequest, context: { params: Promise<{ name: string }> }, @@ -56,14 +52,10 @@ export async function GET( }); if (!res.ok) { const raw = await res.text(); - const response = NextResponse.json( - { - error: "Backend city market scan failed", - upstream_status: res.status, - detail: parseBackendError(raw), - }, - { status: 502 }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 800, + error: "Backend city market scan failed", + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); @@ -74,13 +66,10 @@ export async function GET( }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - const response = NextResponse.json( - { - error: "Failed to fetch city market scan", - detail: String(error), - }, - { status: 502 }, - ); + const response = buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch city market scan", + status: 502, + }); return response; } } diff --git a/frontend/app/api/city/[name]/route.ts b/frontend/app/api/city/[name]/route.ts index 9678079a..44b5c469 100644 --- a/frontend/app/api/city/[name]/route.ts +++ b/frontend/app/api/city/[name]/route.ts @@ -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; @@ -150,20 +154,16 @@ export async function GET( return applyAuthResponseCookies(response, auth.response); } - 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 = normalizeCityDetailPayload(await res.json()); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - const response = NextResponse.json( - { error: "Failed to fetch city detail", detail: String(error) }, - { status: 500 }, - ); + const response = buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch city detail", + }); return response; } } diff --git a/frontend/app/api/city/[name]/summary/route.ts b/frontend/app/api/city/[name]/summary/route.ts index e8f5d634..212a1efb 100644 --- a/frontend/app/api/city/[name]/summary/route.ts +++ b/frontend/app/api/city/[name]/summary/route.ts @@ -3,6 +3,10 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { + buildProxyExceptionResponse, + buildUpstreamErrorResponse, +} from "@/lib/api-proxy"; import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -43,10 +47,7 @@ export async function GET( }); 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(); @@ -65,10 +66,9 @@ export async function GET( ); return applyAuthResponseCookies(response, auth.response); } catch (error) { - const response = NextResponse.json( - { error: "Failed to fetch city summary", detail: String(error) }, - { status: 500 }, - ); + const response = buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch city summary", + }); return response; } } diff --git a/frontend/app/api/healthz/route.ts b/frontend/app/api/healthz/route.ts index 04515f3a..987d22d9 100644 --- a/frontend/app/api/healthz/route.ts +++ b/frontend/app/api/healthz/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -32,9 +33,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch healthz", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch healthz", + }); } } diff --git a/frontend/app/api/history/[name]/route.ts b/frontend/app/api/history/[name]/route.ts index 9dec7f4d..e2ad2e49 100644 --- a/frontend/app/api/history/[name]/route.ts +++ b/frontend/app/api/history/[name]/route.ts @@ -3,6 +3,10 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { + buildProxyExceptionResponse, + buildUpstreamErrorResponse, +} from "@/lib/api-proxy"; import { buildCachedJsonResponse } from "@/lib/http-cache"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -33,10 +37,7 @@ export async function GET( }); 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(); @@ -47,10 +48,9 @@ export async function GET( ); return applyAuthResponseCookies(response, auth.response); } catch (error) { - const response = NextResponse.json( - { error: "Failed to fetch history", detail: String(error) }, - { status: 500 }, - ); + const response = buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch history", + }); return response; } } diff --git a/frontend/app/api/ops/analytics/funnel/route.ts b/frontend/app/api/ops/analytics/funnel/route.ts index 0949e6e0..72b11a9c 100644 --- a/frontend/app/api/ops/analytics/funnel/route.ts +++ b/frontend/app/api/ops/analytics/funnel/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -35,9 +36,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch analytics funnel", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch analytics funnel", + }); } } diff --git a/frontend/app/api/ops/leaderboard/weekly/route.ts b/frontend/app/api/ops/leaderboard/weekly/route.ts index 97bcc6ce..b2559e93 100644 --- a/frontend/app/api/ops/leaderboard/weekly/route.ts +++ b/frontend/app/api/ops/leaderboard/weekly/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -34,9 +35,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch weekly leaderboard", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch weekly leaderboard", + }); } } diff --git a/frontend/app/api/ops/memberships/route.ts b/frontend/app/api/ops/memberships/route.ts index 77d62618..ad38e350 100644 --- a/frontend/app/api/ops/memberships/route.ts +++ b/frontend/app/api/ops/memberships/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -34,9 +35,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch memberships", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch memberships", + }); } } diff --git a/frontend/app/api/ops/payments/incidents/[eventId]/resolve/route.ts b/frontend/app/api/ops/payments/incidents/[eventId]/resolve/route.ts index bcd34dac..93ba3ef9 100644 --- a/frontend/app/api/ops/payments/incidents/[eventId]/resolve/route.ts +++ b/frontend/app/api/ops/payments/incidents/[eventId]/resolve/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -36,9 +37,8 @@ export async function POST(req: NextRequest, context: RouteContext) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to resolve payment incident", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to resolve payment incident", + }); } } diff --git a/frontend/app/api/ops/payments/incidents/route.ts b/frontend/app/api/ops/payments/incidents/route.ts index 25ac3c8c..21a114bf 100644 --- a/frontend/app/api/ops/payments/incidents/route.ts +++ b/frontend/app/api/ops/payments/incidents/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -34,9 +35,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch payment incidents", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch payment incidents", + }); } } diff --git a/frontend/app/api/ops/truth-history/route.ts b/frontend/app/api/ops/truth-history/route.ts index 86e6efa5..8a4d7596 100644 --- a/frontend/app/api/ops/truth-history/route.ts +++ b/frontend/app/api/ops/truth-history/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -36,9 +37,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch truth history", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch truth history", + }); } } diff --git a/frontend/app/api/ops/users/grant-points/route.ts b/frontend/app/api/ops/users/grant-points/route.ts index 5bdd09a3..2e11e476 100644 --- a/frontend/app/api/ops/users/grant-points/route.ts +++ b/frontend/app/api/ops/users/grant-points/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -36,9 +37,8 @@ export async function POST(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to grant points", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to grant points", + }); } } diff --git a/frontend/app/api/ops/users/route.ts b/frontend/app/api/ops/users/route.ts index 7f235168..5b8fab3b 100644 --- a/frontend/app/api/ops/users/route.ts +++ b/frontend/app/api/ops/users/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -36,9 +37,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch ops users", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch ops users", + }); } } diff --git a/frontend/app/api/payments/config/route.ts b/frontend/app/api/payments/config/route.ts index 3de834a2..1ed6e9ae 100644 --- a/frontend/app/api/payments/config/route.ts +++ b/frontend/app/api/payments/config/route.ts @@ -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; @@ -21,10 +25,9 @@ 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, 350) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); @@ -33,10 +36,9 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch payment config", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch payment config", + }); } } diff --git a/frontend/app/api/payments/intents/[intentId]/confirm/route.ts b/frontend/app/api/payments/intents/[intentId]/confirm/route.ts index 6da70365..0a0a2fe1 100644 --- a/frontend/app/api/payments/intents/[intentId]/confirm/route.ts +++ b/frontend/app/api/payments/intents/[intentId]/confirm/route.ts @@ -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; @@ -33,19 +37,17 @@ export async function POST( ); if (!res.ok) { const raw = await res.text(); - const response = NextResponse.json( - { error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to confirm payment tx", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to confirm payment tx", + }); } } diff --git a/frontend/app/api/payments/intents/[intentId]/route.ts b/frontend/app/api/payments/intents/[intentId]/route.ts index 6d9ddcac..bf869afa 100644 --- a/frontend/app/api/payments/intents/[intentId]/route.ts +++ b/frontend/app/api/payments/intents/[intentId]/route.ts @@ -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; @@ -29,19 +33,17 @@ export async function GET( ); if (!res.ok) { const raw = await res.text(); - const response = NextResponse.json( - { error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch payment intent", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch payment intent", + }); } } diff --git a/frontend/app/api/payments/intents/[intentId]/submit/route.ts b/frontend/app/api/payments/intents/[intentId]/submit/route.ts index e6679d5f..f44956f0 100644 --- a/frontend/app/api/payments/intents/[intentId]/submit/route.ts +++ b/frontend/app/api/payments/intents/[intentId]/submit/route.ts @@ -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; @@ -33,19 +37,17 @@ export async function POST( ); if (!res.ok) { const raw = await res.text(); - const response = NextResponse.json( - { error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to submit payment tx", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to submit payment tx", + }); } } diff --git a/frontend/app/api/payments/intents/route.ts b/frontend/app/api/payments/intents/route.ts index 3f1780d4..11f6d478 100644 --- a/frontend/app/api/payments/intents/route.ts +++ b/frontend/app/api/payments/intents/route.ts @@ -3,6 +3,10 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { + buildProxyExceptionResponse, + buildUpstreamErrorResponse, +} from "@/lib/api-proxy"; import { isPaymentHostAllowed } from "@/lib/payment-host"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -41,20 +45,18 @@ 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, 350) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to create payment intent", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to create payment intent", + }); } } diff --git a/frontend/app/api/payments/reconcile-latest/route.ts b/frontend/app/api/payments/reconcile-latest/route.ts index 66d34b4b..52a32837 100644 --- a/frontend/app/api/payments/reconcile-latest/route.ts +++ b/frontend/app/api/payments/reconcile-latest/route.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -31,9 +32,8 @@ export async function POST(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to reconcile latest payment", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to reconcile latest payment", + }); } } diff --git a/frontend/app/api/payments/runtime/route.ts b/frontend/app/api/payments/runtime/route.ts index f941f112..5c97ee05 100644 --- a/frontend/app/api/payments/runtime/route.ts +++ b/frontend/app/api/payments/runtime/route.ts @@ -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; @@ -22,10 +26,9 @@ 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, 500) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 500, + }); return applyAuthResponseCookies(response, auth.response); } @@ -35,9 +38,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch payment runtime", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch payment runtime", + }); } } diff --git a/frontend/app/api/payments/wallets/challenge/route.ts b/frontend/app/api/payments/wallets/challenge/route.ts index eb8d454c..53b29cd7 100644 --- a/frontend/app/api/payments/wallets/challenge/route.ts +++ b/frontend/app/api/payments/wallets/challenge/route.ts @@ -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; @@ -26,30 +30,25 @@ 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, 350), - proxy_debug: { + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + extraDebug: { has_authorization: proxiedHeaders.has("authorization"), has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"), has_forwarded_user_id: proxiedHeaders.has( "x-polyweather-auth-user-id", ), has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"), - }, }, - { status: res.status }, - ); + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to create wallet challenge", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to create wallet challenge", + }); } } diff --git a/frontend/app/api/payments/wallets/route.ts b/frontend/app/api/payments/wallets/route.ts index 8348abc4..2d0d398e 100644 --- a/frontend/app/api/payments/wallets/route.ts +++ b/frontend/app/api/payments/wallets/route.ts @@ -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; @@ -21,10 +25,9 @@ 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, 350) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); @@ -33,10 +36,9 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch wallets", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch wallets", + }); } } @@ -65,11 +67,9 @@ export async function DELETE(req: NextRequest) { }); const raw = await res.text(); if (!res.ok) { - const response = NextResponse.json( - { - error: `Backend returned ${res.status}`, - detail: raw.slice(0, 350), - proxy_debug: { + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + extraDebug: { incoming_has_authorization: Boolean( String(req.headers.get("authorization") || "").trim(), ), @@ -79,10 +79,8 @@ export async function DELETE(req: NextRequest) { "x-polyweather-auth-user-id", ), has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"), - }, }, - { status: res.status }, - ); + }); return applyAuthResponseCookies(response, auth.response); } let data: unknown = { ok: true }; @@ -98,10 +96,9 @@ export async function DELETE(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to unbind wallet", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to unbind wallet", + }); } } diff --git a/frontend/app/api/payments/wallets/verify/route.ts b/frontend/app/api/payments/wallets/verify/route.ts index 55100957..ea182eec 100644 --- a/frontend/app/api/payments/wallets/verify/route.ts +++ b/frontend/app/api/payments/wallets/verify/route.ts @@ -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; @@ -26,31 +30,26 @@ 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, 350), - proxy_debug: { + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 350, + extraDebug: { has_authorization: proxiedHeaders.has("authorization"), has_entitlement: proxiedHeaders.has("x-polyweather-entitlement"), has_forwarded_user_id: proxiedHeaders.has( "x-polyweather-auth-user-id", ), has_forwarded_email: proxiedHeaders.has("x-polyweather-auth-email"), - }, }, - { status: res.status }, - ); + }); return applyAuthResponseCookies(response, auth.response); } const data = await res.json(); const response = NextResponse.json(data); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to verify wallet binding", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to verify wallet binding", + }); } } diff --git a/frontend/app/api/scan/terminal/ai-city/route.ts b/frontend/app/api/scan/terminal/ai-city/route.ts index e404f409..f42e76b4 100644 --- a/frontend/app/api/scan/terminal/ai-city/route.ts +++ b/frontend/app/api/scan/terminal/ai-city/route.ts @@ -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); diff --git a/frontend/app/api/scan/terminal/ai-city/stream/route.ts b/frontend/app/api/scan/terminal/ai-city/stream/route.ts index 76391b9c..ebcba9b0 100644 --- a/frontend/app/api/scan/terminal/ai-city/stream/route.ts +++ b/frontend/app/api/scan/terminal/ai-city/stream/route.ts @@ -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); } } diff --git a/frontend/app/api/scan/terminal/ai/route.ts b/frontend/app/api/scan/terminal/ai/route.ts index 616bf616..847537de 100644 --- a/frontend/app/api/scan/terminal/ai/route.ts +++ b/frontend/app/api/scan/terminal/ai/route.ts @@ -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); diff --git a/frontend/app/api/scan/terminal/route.ts b/frontend/app/api/scan/terminal/route.ts index bb1c4ccd..f9f6f12c 100644 --- a/frontend/app/api/scan/terminal/route.ts +++ b/frontend/app/api/scan/terminal/route.ts @@ -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); diff --git a/frontend/app/api/system/status/route.ts b/frontend/app/api/system/status/route.ts index 5c8273da..70e2a88d 100644 --- a/frontend/app/api/system/status/route.ts +++ b/frontend/app/api/system/status/route.ts @@ -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; @@ -22,10 +26,9 @@ 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, 500) }, - { status: res.status }, - ); + const response = buildUpstreamErrorResponse(res.status, raw, { + detailLimit: 500, + }); return applyAuthResponseCookies(response, auth.response); } @@ -35,9 +38,8 @@ export async function GET(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { error: "Failed to fetch system status", detail: String(error) }, - { status: 500 }, - ); + return buildProxyExceptionResponse(error, { + publicMessage: "Failed to fetch system status", + }); } } diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index f8ff185c..6d351127 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -31,7 +31,6 @@ import modalChromeStyles from "./ModalChrome.module.css"; import scanTerminalCalendarStyles from "./ScanTerminalCalendar.module.css"; import scanTerminalCardStyles from "./ScanTerminalCard.module.css"; import scanTerminalLightThemeStyles from "./ScanTerminalLightTheme.module.css"; -import scanTerminalMobileStyles from "./ScanTerminalMobile.module.css"; import scanTerminalOpportunityStyles from "./ScanTerminalOpportunity.module.css"; import scanTerminalStyles from "./ScanTerminal.module.css"; import scanTerminalBoardStyles from "./ScanTerminalBoard.module.css"; @@ -40,6 +39,7 @@ import scanTerminalFiltersStyles from "./ScanTerminalFilters.module.css"; import scanTerminalListStyles from "./ScanTerminalList.module.css"; import scanTerminalShellStyles from "./ScanTerminalShell.module.css"; import scanTerminalStateStyles from "./ScanTerminalState.module.css"; +import scanTerminalMobileStyles from "./ScanTerminalMobile.module.css"; import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall"; import { DashboardStoreProvider, diff --git a/frontend/components/dashboard/ScanTerminalMobile.module.css b/frontend/components/dashboard/ScanTerminalMobile.module.css index b7ae2cea..1874b17f 100644 --- a/frontend/components/dashboard/ScanTerminalMobile.module.css +++ b/frontend/components/dashboard/ScanTerminalMobile.module.css @@ -144,6 +144,8 @@ @media (max-width: 1100px) { .root :global(.scan-terminal) { grid-template-columns: 1fr; + min-height: 100dvh; + overflow-x: hidden; } .root :global(.scan-filter-panel), @@ -153,6 +155,27 @@ min-height: auto; } + .root :global(.scan-data-grid) { + max-height: none; + overflow: visible; + } + + .root :global(.scan-list-section), + .root :global(.scan-table-shell), + .root :global(.scan-ai-workspace) { + overflow: visible; + } + + .root :global(.scan-ai-workspace) { + max-height: none; + } + + .root :global(.scan-terminal > .detail-panel.scan-city-detail-rail) { + width: 100%; + min-width: 0; + max-width: 100%; + } + .root :global(.scan-kpi-bar) { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -267,6 +290,26 @@ padding: 10px; } + .root :global(.scan-data-grid) { + padding: 12px; + gap: 14px; + border-radius: 18px; + } + + .root :global(.scan-topbar-actions) { + width: 100%; + justify-content: flex-start; + gap: 8px; + } + + .root :global(.scan-topbar-title strong) { + font-size: 23px; + } + + .root :global(.scan-topbar-title span) { + font-size: 13px; + } + .root :global(.scan-kpi-bar) { grid-template-columns: 1fr; } @@ -389,6 +432,40 @@ align-items: flex-start; } + .root :global(.scan-list-tabs) { + width: 100%; + gap: 8px; + overflow-x: auto; + padding-bottom: 4px; + scrollbar-width: none; + } + + .root :global(.scan-list-tabs::-webkit-scrollbar) { + display: none; + } + + .root :global(.scan-list-tabs button) { + flex: 0 0 auto; + min-height: 36px; + border: 1px solid rgba(99, 132, 180, 0.18); + border-radius: 999px; + background: rgba(8, 19, 34, 0.76); + padding: 0 12px; + font-size: 13px; + white-space: nowrap; + } + + .root :global(.scan-list-tabs button.active) { + border-color: rgba(77, 163, 255, 0.38); + background: rgba(77, 163, 255, 0.16); + } + + .root :global(.scan-list-status) { + width: 100%; + flex-wrap: wrap; + gap: 8px; + } + .root :global(.scan-opportunity-groups) { padding: 12px; } @@ -411,3 +488,50 @@ padding: 0 13px 13px; } } + +@media (max-width: 520px) { + .root :global(.scan-terminal) { + padding: 6px; + } + + .root :global(.scan-data-grid) { + padding: 10px; + border-radius: 16px; + } + + .root :global(.scan-upgrade-announcement) { + margin: -2px 0 2px; + padding: 12px; + border-radius: 16px; + } + + .root :global(.scan-upgrade-announcement-copy p) { + display: none; + } + + .root :global(.scan-upgrade-announcement ul) { + display: none; + } + + .root :global(.scan-status-chip) { + height: 32px; + padding: 0 10px; + font-size: 11px; + } + + .root :global(.scan-primary-button) { + min-height: 38px; + padding: 9px 12px; + font-size: 13px; + } + + .root :global(.scan-account-button) { + width: 38px; + height: 38px; + } + + .root :global(.scan-theme-button) { + width: 38px; + height: 38px; + } +} diff --git a/frontend/lib/api-proxy.ts b/frontend/lib/api-proxy.ts new file mode 100644 index 00000000..a02688d3 --- /dev/null +++ b/frontend/lib/api-proxy.ts @@ -0,0 +1,72 @@ +import { NextResponse } from "next/server"; + +const PASSTHROUGH_UPSTREAM_STATUSES = new Set([ + 400, + 401, + 402, + 403, + 404, + 409, + 422, + 429, +]); + +function shouldExposeProxyErrorDetail() { + return ( + process.env.NODE_ENV !== "production" || + process.env.POLYWEATHER_EXPOSE_PROXY_ERROR_DETAIL === "true" + ); +} + +export function clientStatusFromUpstream(status: number) { + if (PASSTHROUGH_UPSTREAM_STATUSES.has(status)) { + return status; + } + return 502; +} + +export function buildUpstreamErrorResponse( + upstreamStatus: number, + rawDetail: string, + options?: { + detailLimit?: number; + error?: string; + extraDebug?: Record; + }, +) { + const body: Record = { + error: options?.error || "Upstream request failed", + upstream_status: upstreamStatus, + }; + + if (shouldExposeProxyErrorDetail()) { + body.detail = String(rawDetail || "").slice(0, options?.detailLimit ?? 300); + if (options?.extraDebug) { + body.proxy_debug = options.extraDebug; + } + } + + return NextResponse.json(body, { + status: clientStatusFromUpstream(upstreamStatus), + }); +} + +export function buildProxyExceptionResponse( + error: unknown, + options: { + status?: number; + publicMessage: string; + extra?: Record; + }, +) { + const body: Record = { + error: options.publicMessage, + ...(options.extra || {}), + }; + + if (shouldExposeProxyErrorDetail()) { + body.detail = String(error); + } + + return NextResponse.json(body, { status: options.status ?? 500 }); +} diff --git a/frontend/lib/system-priority-proxy.ts b/frontend/lib/system-priority-proxy.ts index b0357fc4..082c28de 100644 --- a/frontend/lib/system-priority-proxy.ts +++ b/frontend/lib/system-priority-proxy.ts @@ -3,6 +3,7 @@ import { applyAuthResponseCookies, buildBackendRequestHeaders, } from "@/lib/backend-auth"; +import { buildProxyExceptionResponse } from "@/lib/api-proxy"; const API_BASE = process.env.POLYWEATHER_API_BASE_URL; @@ -47,15 +48,15 @@ export async function forwardPriorityWarmHint(req: NextRequest) { if (!res.ok) { const raw = await res.text(); - const response = NextResponse.json( - { + const response = buildProxyExceptionResponse(raw, { + status: 202, + publicMessage: "Priority warm hint was skipped", + extra: { ok: false, skipped: true, - error: `Backend returned ${res.status}`, - detail: raw.slice(0, 500), + upstream_status: res.status, }, - { status: 202 }, - ); + }); return applyAuthResponseCookies(response, auth.response); } @@ -65,14 +66,13 @@ export async function forwardPriorityWarmHint(req: NextRequest) { }); return applyAuthResponseCookies(response, auth.response); } catch (error) { - return NextResponse.json( - { + return buildProxyExceptionResponse(error, { + status: 202, + publicMessage: "Failed to send priority warm hint", + extra: { ok: false, skipped: true, - error: "Failed to send priority warm hint", - detail: String(error), }, - { status: 202 }, - ); + }); } }