fix(backend): use v2 structured search API for OFF
Run Tests on Branches / Detect Changes (push) Successful in 14s
Run Tests on Branches / Frontend Tests (push) Successful in 3m25s
Run Tests on Branches / Backend Tests (push) Has been cancelled
Run Tests on Branches / Frontend Mobile Tests (push) Has been cancelled

- 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
This commit is contained in:
Antoni Nuñez Romeu
2026-07-13 17:00:09 +02:00
parent 452a835b64
commit 20debdb023
2 changed files with 23 additions and 8 deletions
+3 -1
View File
@@ -20,7 +20,9 @@ VAPID_SUBJECT=mailto:admin@example.com
# https://expo.dev/accounts/[username]/settings/access-tokens # https://expo.dev/accounts/[username]/settings/access-tokens
EXPO_ACCESS_TOKEN= EXPO_ACCESS_TOKEN=
# Open Food Facts (optional, improves rate limits) # Open Food Facts
# Register at: https://world.openfoodfacts.org/ # 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_USERNAME=
OFF_PASSWORD= OFF_PASSWORD=
+20 -7
View File
@@ -1,15 +1,25 @@
import axios from 'axios'; import axios from 'axios';
import redisClient from './redis-client.js'; 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 OFF_API_BASE = 'https://world.openfoodfacts.org';
const CACHE_TTL = 3600; const CACHE_TTL = 3600;
const MAX_RETRIES = 2; const MAX_RETRIES = 2;
const RETRY_DELAY_MS = 1000; 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_USERNAME = process.env.OFF_USERNAME;
const OFF_PASSWORD = process.env.OFF_PASSWORD; const OFF_PASSWORD = process.env.OFF_PASSWORD;
function getOffHeaders() {
return {
'User-Agent': OFF_USER_AGENT,
};
}
function getOffAuth() { function getOffAuth() {
if (OFF_USERNAME && OFF_PASSWORD) { if (OFF_USERNAME && OFF_PASSWORD) {
return { return {
@@ -74,18 +84,18 @@ export async function searchBabyProducts(query) {
console.log(`Fetching from OFF API: ${searchTerm}`); console.log(`Fetching from OFF API: ${searchTerm}`);
const auth = getOffAuth(); const auth = getOffAuth();
const headers = getOffHeaders();
let lastError; let lastError;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try { 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: { params: {
search_terms: searchTerm, brands_tags: searchTerm,
json: true,
fields: 'product_name,brands,image_url,nutriscore_grade,ingredients_text,nova_group,ecoscore_grade,categories_tags',
page_size: 20, page_size: 20,
action: 'process',
}, },
headers,
timeout: 10000, timeout: 10000,
validateStatus: (status) => status === 200, validateStatus: (status) => status === 200,
...(auth && { auth }), ...(auth && { auth }),
@@ -150,7 +160,10 @@ export async function getBabyProductDetails(barcode) {
console.log(`Fetching OFF product details: ${barcode}`); console.log(`Fetching OFF product details: ${barcode}`);
const auth = getOffAuth(); 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, timeout: 10000,
validateStatus: (status) => status === 200, validateStatus: (status) => status === 200,
...(auth && { auth }), ...(auth && { auth }),