Skip to content

Commit cc3ce16

Browse files
authored
fix: Enhance error handling in API responses and update routing for p… (#72)
2 parents 9dd4869 + f116113 commit cc3ce16

9 files changed

Lines changed: 75 additions & 22 deletions

File tree

backend/devops/backend-services/security/modsecurity/default.conf

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,13 @@ server {
179179
proxy_pass http://friends_notifications_ws_upstream;
180180
}
181181

182-
location /plan {
182+
# SPA route must not use prefix /plan (would match /planner → wrong upstream).
183+
location = /planner {
184+
proxy_pass http://frontend_upstream;
185+
}
186+
187+
# Planner API: /plan/generate, /plan/:id, /plan/:id/export/...
188+
location /plan/ {
183189
proxy_pass http://planner_upstream;
184190
}
185191

backend/src/planner-service/src/lib/gemini.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,21 @@ async function generateTripPlan({
4545

4646
const jsonMatch = text.match(/\{[\s\S]*\}/);
4747
if (!jsonMatch) {
48-
throw new Error('Gemini returned no valid JSON in response');
48+
const preview = String(text).slice(0, 280).replace(/\s+/g, ' ').trim();
49+
throw new Error(
50+
`Gemini returned no JSON object (expected a single {...} trip plan). Preview: ${preview}`,
51+
);
4952
}
5053

51-
const plan = JSON.parse(jsonMatch[0]);
54+
let plan;
55+
try {
56+
plan = JSON.parse(jsonMatch[0]);
57+
} catch (e) {
58+
const msg = e instanceof Error ? e.message : String(e);
59+
throw new Error(
60+
`Gemini JSON was not parseable (${msg}). Snippet: ${jsonMatch[0].slice(0, 200)}`,
61+
);
62+
}
5263

5364
if (!plan.days || !Array.isArray(plan.days)) {
5465
throw new Error('Gemini response missing required "days" array');

frontend/package-lock.json

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/components/shared/HomeNavBar.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
isolation: isolate;
1414
}
1515

16-
/* Liquid glass header surface */
16+
/* Liquid glass header surface
1717
.home-nav::before {
1818
content: "";
1919
position: absolute;
@@ -41,7 +41,7 @@
4141
filter: blur(18px);
4242
opacity: 0.55;
4343
animation: homeNavSheen 10s ease-in-out infinite;
44-
}
44+
} */
4545

4646
@keyframes homeNavSheen {
4747
0% { transform: translate3d(-4%, -2%, 0) rotate(-6deg); }
@@ -63,7 +63,7 @@
6363

6464
/* Home video: lighter glass so the video reads through */
6565
.home-nav.home-nav--home-video::before {
66-
background: rgba(10, 10, 12, 0.28);
66+
/* background: rgba(10, 10, 12, 0.28); */
6767
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
6868
box-shadow:
6969
0 10px 30px rgba(0, 0, 0, 0.30),

frontend/src/components/shared/HomeNavBar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const PATH_TO_NAV_ID: Record<string, string> = {
2121
};
2222

2323
type HomeNavBarProps = {
24-
/** Hide bottom glass pill on small viewports (e.g. active chat thread). */
2524
hideMobileGlassNav?: boolean;
2625
};
2726

frontend/src/components/shared/TripPlannerBar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import GlassSearchBar from "./GlassSearchBar";
55
import GlassCalendar from "./GlassCalendar";
66
import { useAuth } from "../../context/AuthContext";
77
import "./TripPlannerBar.css";
8-
import { resolveGatewayUrl } from "@/lib/api";
8+
import { parseApiJson, resolveGatewayUrl } from "@/lib/api";
99

1010
const PLANNER_URL = resolveGatewayUrl(
1111
import.meta.env.VITE_PLANNER_URL as string | undefined,
@@ -165,7 +165,8 @@ function TripPlannerBar({
165165
}),
166166
});
167167

168-
const data = await res.json();
168+
const raw = await parseApiJson(res);
169+
const data = raw as { ok?: boolean; error?: { message?: string }; data?: TripPlan };
169170

170171
if (!data.ok) {
171172
throw new Error(data.error?.message ?? "Failed to generate plan");

frontend/src/lib/api.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,30 @@ export async function ensureCsrfToken(): Promise<string> {
4141
export function clearCachedCsrfToken() {
4242
cachedCsrfToken = null;
4343
}
44+
45+
/**
46+
* Read response body as text and parse JSON. Avoids `res.json()` throwing
47+
* `Unexpected token '<'` when nginx/upstream returns an HTML error page.
48+
*/
49+
export async function parseApiJson(res: Response): Promise<unknown> {
50+
const text = await res.text();
51+
const t = text.trim();
52+
if (!t) return null;
53+
const looksHtml = /^<!DOCTYPE/i.test(t) || /^<html/i.test(t) || t.startsWith('<h');
54+
if (looksHtml) {
55+
const hint =
56+
res.status >= 500
57+
? ' Planner may be down or the gateway returned an error page.'
58+
: ' Check that the URL hits the API (e.g. /plan/... under https://localhost), not the SPA.';
59+
throw new Error(
60+
`Server returned HTML instead of JSON (HTTP ${res.status}).${hint}`,
61+
);
62+
}
63+
try {
64+
return JSON.parse(t);
65+
} catch {
66+
throw new Error(
67+
`Invalid JSON (HTTP ${res.status}): ${t.slice(0, 180)}${t.length > 180 ? '…' : ''}`,
68+
);
69+
}
70+
}

frontend/src/pages/PlannerPage.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { needsInterestsOnboarding } from "@/lib/interestsOnboarding";
1111
import { useTheme } from "@/context/ThemeContext";
1212
import BackArrow from "@/components/shared/BackArrow";
1313
import GoogleCalendar from "@/components/shared/GoogleCalendar";
14-
import { resolveGatewayUrl } from "@/lib/api";
14+
import { parseApiJson, resolveGatewayUrl } from "@/lib/api";
1515

1616
const PLANNER_URL =
1717
resolveGatewayUrl(import.meta.env.VITE_PLANNER_URL as string | undefined);
@@ -400,9 +400,16 @@ function PlannerSidebar({
400400

401401
async function parseJson(res: Response): Promise<{ ok: boolean; data?: unknown; error?: { message?: string } }> {
402402
try {
403-
return await res.json();
404-
} catch {
405-
return { ok: false, error: { message: "Invalid response" } };
403+
const raw = await parseApiJson(res);
404+
if (raw == null) {
405+
return { ok: false, error: { message: "Empty response from server" } };
406+
}
407+
return raw as { ok: boolean; data?: unknown; error?: { message?: string } };
408+
} catch (e) {
409+
return {
410+
ok: false,
411+
error: { message: e instanceof Error ? e.message : "Invalid response" },
412+
};
406413
}
407414
}
408415

frontend/src/pages/SavedPlacesPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import HomeNavBar from "@/components/shared/HomeNavBar";
44
import { useAuth } from "@/context/AuthContext";
55
import { AI_PLACES_URL, resolvePlaceImageUrl } from "@/hooks/useCityPlaces";
66
import { cn } from "@/lib/utils";
7-
import { resolveGatewayUrl } from "@/lib/api";
7+
import { parseApiJson, resolveGatewayUrl } from "@/lib/api";
88
import {
99
MapPin,
1010
Star,
@@ -553,7 +553,7 @@ function SavedPlacesPage() {
553553
setTripsLoading(true);
554554
setTripsError(null);
555555
fetch(`${PLANNER_URL}/plans`, { credentials: "include" })
556-
.then((r) => r.json())
556+
.then(async (r) => (await parseApiJson(r)) as PlansEnvelope)
557557
.then((env: PlansEnvelope) => {
558558
if (!env.ok) throw new Error(env.error?.message ?? "Failed to load saved trips");
559559
setTrips(env.data ?? []);

0 commit comments

Comments
 (0)