From 20debdb02366389cd99d51b66e2d98856b2fb1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 13 Jul 2026 17:00:09 +0200 Subject: [PATCH] fix(backend): use v2 structured search API for OFF - Switch from /cgi/search.pl (503 errors) to /api/v2/search with brand filter - Add required User-Agent header per OFF documentation - Add OFF_USER_AGENT env var for customization - Update .env.example with User-Agent documentation - Retry logic continues to work with new endpoint --- apps/backend/.env.example | 4 +++- apps/backend/off-service.js | 27 ++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) 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 }),