diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 2b28232..b681f3f 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -20,7 +20,9 @@ VAPID_SUBJECT=mailto:admin@example.com # https://expo.dev/accounts/[username]/settings/access-tokens EXPO_ACCESS_TOKEN= -# Open Food Facts (optional, improves rate limits) +# Open Food Facts # Register at: https://world.openfoodfacts.org/ +# User-Agent is REQUIRED per OFF docs (format: AppName/Version (ContactEmail)) +OFF_USER_AGENT=FarmaFinder/1.0 (https://github.com/farmafinder) OFF_USERNAME= OFF_PASSWORD= diff --git a/apps/backend/off-service.js b/apps/backend/off-service.js index 110227c..9dde7ad 100644 --- a/apps/backend/off-service.js +++ b/apps/backend/off-service.js @@ -1,15 +1,25 @@ import axios from 'axios'; import redisClient from './redis-client.js'; +// OFF API v3 (recommended) with fallback to v2 for search const OFF_API_BASE = 'https://world.openfoodfacts.org'; const CACHE_TTL = 3600; const MAX_RETRIES = 2; const RETRY_DELAY_MS = 1000; -// Open Food Facts authentication (required for reliable access) +// User-Agent is REQUIRED per OFF docs: AppName/Version (ContactEmail) +const OFF_USER_AGENT = process.env.OFF_USER_AGENT || 'FarmaFinder/1.0 (https://github.com/farmafinder)'; + +// Open Food Facts authentication (optional, for write operations) const OFF_USERNAME = process.env.OFF_USERNAME; const OFF_PASSWORD = process.env.OFF_PASSWORD; +function getOffHeaders() { + return { + 'User-Agent': OFF_USER_AGENT, + }; +} + function getOffAuth() { if (OFF_USERNAME && OFF_PASSWORD) { return { @@ -74,18 +84,18 @@ export async function searchBabyProducts(query) { console.log(`Fetching from OFF API: ${searchTerm}`); const auth = getOffAuth(); + const headers = getOffHeaders(); let lastError; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { try { - const response = await axios.get(`${OFF_API_BASE}/cgi/search.pl`, { + // Use v2 structured search with brand filter (more reliable than /cgi/search.pl) + const response = await axios.get(`${OFF_API_BASE}/api/v2/search`, { params: { - search_terms: searchTerm, - json: true, - fields: 'product_name,brands,image_url,nutriscore_grade,ingredients_text,nova_group,ecoscore_grade,categories_tags', + brands_tags: searchTerm, page_size: 20, - action: 'process', }, + headers, timeout: 10000, validateStatus: (status) => status === 200, ...(auth && { auth }), @@ -150,7 +160,10 @@ export async function getBabyProductDetails(barcode) { console.log(`Fetching OFF product details: ${barcode}`); const auth = getOffAuth(); - const response = await axios.get(`${OFF_API_BASE}/api/v2/product/${barcode}.json`, { + const headers = getOffHeaders(); + // Use v3 API (recommended) for product details + const response = await axios.get(`${OFF_API_BASE}/api/v3/product/${barcode}.json`, { + headers, timeout: 10000, validateStatus: (status) => status === 200, ...(auth && { auth }),