Skip to content

Commit 8782ccb

Browse files
committed
fix: prevent app crash when ingredient image base url is missing
1 parent 64cb2dc commit 8782ccb

File tree

2 files changed

+62
-15
lines changed

2 files changed

+62
-15
lines changed

src/utils/ingredientVisuals.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,43 @@ function normalizeCatalogImageBaseUrl(rawBaseUrl: string): string {
8989
return trimmedValue.replace(/\/+$/u, '');
9090
}
9191

92-
function getCatalogImageBaseUrl(): string {
92+
function getCatalogImageBaseUrlFromEnv(): string | undefined {
9393
const viteEnvCandidate = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;
9494
const processEnvCandidate = typeof process !== 'undefined' ? process.env : undefined;
95-
const configuredBaseUrl = viteEnvCandidate?.VITE_INGREDIENT_IMAGE_BASE_URL ?? processEnvCandidate?.VITE_INGREDIENT_IMAGE_BASE_URL;
95+
return viteEnvCandidate?.VITE_INGREDIENT_IMAGE_BASE_URL ?? processEnvCandidate?.VITE_INGREDIENT_IMAGE_BASE_URL;
96+
}
97+
98+
let cachedCatalogImageBaseUrlConfig: string | undefined;
99+
let cachedCatalogImageBaseUrlValue: string | null | undefined;
100+
101+
function getCatalogImageBaseUrl(): string | null {
102+
const configuredBaseUrl = getCatalogImageBaseUrlFromEnv();
103+
if (cachedCatalogImageBaseUrlValue !== undefined && cachedCatalogImageBaseUrlConfig === configuredBaseUrl) {
104+
return cachedCatalogImageBaseUrlValue;
105+
}
96106

97107
if (configuredBaseUrl === undefined) {
98-
throw new Error('VITE_INGREDIENT_IMAGE_BASE_URL is required for ingredient catalog images.');
108+
cachedCatalogImageBaseUrlConfig = configuredBaseUrl;
109+
cachedCatalogImageBaseUrlValue = null;
110+
return cachedCatalogImageBaseUrlValue;
99111
}
100112

101-
return normalizeCatalogImageBaseUrl(configuredBaseUrl);
113+
try {
114+
cachedCatalogImageBaseUrlConfig = configuredBaseUrl;
115+
cachedCatalogImageBaseUrlValue = normalizeCatalogImageBaseUrl(configuredBaseUrl);
116+
return cachedCatalogImageBaseUrlValue;
117+
} catch (error) {
118+
console.warn(
119+
'Ingredient catalog images are disabled because VITE_INGREDIENT_IMAGE_BASE_URL is invalid.',
120+
{
121+
configuredBaseUrl,
122+
error: error instanceof Error ? error.message : String(error),
123+
},
124+
);
125+
cachedCatalogImageBaseUrlConfig = configuredBaseUrl;
126+
cachedCatalogImageBaseUrlValue = null;
127+
return cachedCatalogImageBaseUrlValue;
128+
}
102129
}
103130

104131
function createIngredientCatalogEntry<TKey extends string>(
@@ -640,8 +667,9 @@ export function resolveIngredientVisual(input: IngredientVisualInput): Ingredien
640667
const catalogMatch = findIngredientCatalogMatch(searchText);
641668
if (catalogMatch !== null) {
642669
const imageBaseUrl = getCatalogImageBaseUrl();
670+
const imageUrl = imageBaseUrl === null ? null : buildCatalogImageUrl(imageBaseUrl, catalogMatch.entry.imageObjectKey);
643671
return {
644-
imageUrl: buildCatalogImageUrl(imageBaseUrl, catalogMatch.entry.imageObjectKey),
672+
imageUrl,
645673
fallbackIcon: catalogMatch.entry.fallbackIcon,
646674
altText: catalogMatch.entry.altText,
647675
source: 'catalog-match',

test/unit/run.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,19 +217,37 @@ function testIngredientVisualCatalogMatch(): void {
217217
assert.equal(visual.imageUrl, 'https://cdn.example.com/rasoi/ingredients/turmeric.webp');
218218
}
219219

220-
function testIngredientVisualRequiresConfiguredBaseUrl(): void {
220+
function testIngredientVisualMissingBaseUrlUsesFallbackWithoutThrowing(): void {
221221
const previousValue = process.env.VITE_INGREDIENT_IMAGE_BASE_URL;
222222
delete process.env.VITE_INGREDIENT_IMAGE_BASE_URL;
223223

224224
try {
225-
assert.throws(
226-
() =>
227-
resolveIngredientVisual({
228-
name: 'Turmeric',
229-
category: 'spices',
230-
}),
231-
/VITE_INGREDIENT_IMAGE_BASE_URL is required/,
232-
);
225+
const visual = resolveIngredientVisual({
226+
name: 'Turmeric',
227+
category: 'spices',
228+
});
229+
230+
assert.equal(visual.source, 'catalog-match');
231+
assert.equal(visual.fallbackIcon, '🟡');
232+
assert.equal(visual.imageUrl, null);
233+
} finally {
234+
process.env.VITE_INGREDIENT_IMAGE_BASE_URL = previousValue;
235+
}
236+
}
237+
238+
function testIngredientVisualInvalidBaseUrlUsesFallbackWithoutThrowing(): void {
239+
const previousValue = process.env.VITE_INGREDIENT_IMAGE_BASE_URL;
240+
process.env.VITE_INGREDIENT_IMAGE_BASE_URL = 'ftp://invalid';
241+
242+
try {
243+
const visual = resolveIngredientVisual({
244+
name: 'Turmeric',
245+
category: 'spices',
246+
});
247+
248+
assert.equal(visual.source, 'catalog-match');
249+
assert.equal(visual.fallbackIcon, '🟡');
250+
assert.equal(visual.imageUrl, null);
233251
} finally {
234252
process.env.VITE_INGREDIENT_IMAGE_BASE_URL = previousValue;
235253
}
@@ -327,7 +345,8 @@ function run(): void {
327345
testBuildPantryLogIsDeterministic();
328346
testSanitizeFirestorePayloadOmitsUndefinedFields();
329347
testIngredientVisualCatalogMatch();
330-
testIngredientVisualRequiresConfiguredBaseUrl();
348+
testIngredientVisualMissingBaseUrlUsesFallbackWithoutThrowing();
349+
testIngredientVisualInvalidBaseUrlUsesFallbackWithoutThrowing();
331350
testIngredientVisualHindiMatch();
332351
testExpandedIngredientVisualCoverage();
333352
testIngredientVisualChoosesMostSpecificKeyword();

0 commit comments

Comments
 (0)