From 6ffaaf17dbea065e133344d4315ffb6cca825512 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Wed, 23 Sep 2026 22:03:10 -0400 Subject: [PATCH 1/3] edge fns: 429 -> 503 rate-limited, warn on DEMO_KEY fallback Co-Authored-By: Claude Opus 5.5 (1M context) --- supabase/functions/food-details/index.ts | 8 +++++++- supabase/functions/search-food/index.ts | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/supabase/functions/food-details/index.ts b/supabase/functions/food-details/index.ts index b9aa3f7..6f7b7e1 100644 --- a/supabase/functions/food-details/index.ts +++ b/supabase/functions/food-details/index.ts @@ -14,7 +14,9 @@ Deno.serve(async (req) => { return jsonResponse({ detail: "food_id must be provided as a path segment" }, { status: 422 }, origin); } - const usdaApiKey = Deno.env.get("USDA_API_KEY") ?? "DEMO_KEY"; + const envKey = Deno.env.get("USDA_API_KEY"); + if (!envKey) console.warn("[FOOD DETAILS] USDA_API_KEY not set, falling back to DEMO_KEY (heavily rate-limited)"); + const usdaApiKey = envKey ?? "DEMO_KEY"; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10_000); @@ -34,6 +36,10 @@ Deno.serve(async (req) => { clearTimeout(timeoutId); } + if (usdaRes.status === 429) { + return jsonResponse({ detail: "Food database rate-limited, try again shortly" }, { status: 503 }, origin); + } + if (!usdaRes.ok) { return jsonResponse({ detail: "Food database unavailable" }, { status: 500 }, origin); } diff --git a/supabase/functions/search-food/index.ts b/supabase/functions/search-food/index.ts index c95510b..1a9680a 100644 --- a/supabase/functions/search-food/index.ts +++ b/supabase/functions/search-food/index.ts @@ -18,7 +18,9 @@ Deno.serve(async (req) => { return jsonResponse({ foods: [] }, { status: 200 }, origin); } - const usdaApiKey = Deno.env.get("USDA_API_KEY") ?? "DEMO_KEY"; + const envKey = Deno.env.get("USDA_API_KEY"); + if (!envKey) console.warn("[FOOD SEARCH] USDA_API_KEY not set, falling back to DEMO_KEY (heavily rate-limited)"); + const usdaApiKey = envKey ?? "DEMO_KEY"; console.log(`[FOOD SEARCH] Searching for: ${query}`); @@ -50,6 +52,10 @@ Deno.serve(async (req) => { console.log(`[FOOD SEARCH] Status: ${usdaRes.status}`); + if (usdaRes.status === 429) { + return jsonResponse({ detail: "Food database rate-limited, try again shortly" }, { status: 503 }, origin); + } + if (!usdaRes.ok) { const errText = await usdaRes.text(); console.log(`[FOOD SEARCH] Error response: ${errText}`); From 26939dbc3d8cb7234db3cfb6cd7ae3cf8867e7b5 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Wed, 23 Sep 2026 22:06:53 -0400 Subject: [PATCH 2/3] FoodSearchInput: error state, abort + request-id guard vs stale responses Co-Authored-By: Claude Opus 5.5 (1M context) --- frontend/src/components/FoodSearchInput.jsx | 69 +++++++++++++++------ 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/FoodSearchInput.jsx b/frontend/src/components/FoodSearchInput.jsx index 716c542..223260d 100644 --- a/frontend/src/components/FoodSearchInput.jsx +++ b/frontend/src/components/FoodSearchInput.jsx @@ -10,7 +10,9 @@ export default function FoodSearchInput({ onFoodSelect, initialValue = '' }) { const [loading, setLoading] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const [quantity, setQuantity] = useState(1); + const [error, setError] = useState(null); const dropdownRef = useRef(null); + const requestIdRef = useRef(0); useEffect(() => { const handleClickOutside = (event) => { @@ -35,33 +37,48 @@ export default function FoodSearchInput({ onFoodSelect, initialValue = '' }) { }, []); useEffect(() => { + setError(null); if (query.length < 2) { + requestIdRef.current += 1; setResults([]); setCustomFoods([]); + setLoading(false); return; } + const controller = new AbortController(); const timer = setTimeout(async () => { + const requestId = ++requestIdRef.current; + const isStale = () => controller.signal.aborted || requestId !== requestIdRef.current; setLoading(true); try { const [dbResponse, customResponse] = await Promise.all([ - api.get(`/search-food?query=${encodeURIComponent(query)}`), + api.get(`/search-food?query=${encodeURIComponent(query)}`, { signal: controller.signal }), searchCustomFoods(query) ]); + if (isStale()) return; setResults(dbResponse.data.foods || []); setCustomFoods(customResponse); + setError(null); setShowDropdown(true); - } catch (error) { - console.error('[FoodSearch] Search failed:', error); + } catch (err) { + const canceled = err?.name === 'CanceledError' || err?.code === 'ERR_CANCELED'; + if (canceled || isStale()) return; + console.error('[FoodSearch] Search failed:', err); setResults([]); setCustomFoods([]); + setShowDropdown(false); + setError('Search unavailable, try again'); } finally { - setLoading(false); + if (requestId === requestIdRef.current) setLoading(false); } }, 500); - return () => clearTimeout(timer); + return () => { + clearTimeout(timer); + controller.abort(); + }; }, [query]); const searchCustomFoods = async (searchQuery) => { @@ -111,6 +128,18 @@ export default function FoodSearchInput({ onFoodSelect, initialValue = '' }) { setQuantity(1); }; + const handleAddManually = () => { + onFoodSelect({ + name: query, + portion: `${quantity} serving`, + calories: 0, + protein_g: 0, + carbs_g: 0, + fat_g: 0, + fiber_g: 0, + }); + }; + return (
@@ -204,24 +233,28 @@ export default function FoodSearchInput({ onFoodSelect, initialValue = '' }) {
)} - {showDropdown && !loading && results.length === 0 && customFoods.length === 0 && query.length >= 2 && ( + {error && !loading && query.length >= 2 && ( +
+

+ {error} +

+ +
+ )} + + {showDropdown && !error && !loading && results.length === 0 && customFoods.length === 0 && query.length >= 2 && (

No foods found. Try a different search term or add manually: