fix: cache JSON concorrente, validação do sync e timeout nas chamadas ao Open Food Facts
- JsonCacheManager: a leitura inicial concorrente carregava o arquivo duas vezes e a segunda descartava as alterações da primeira; gravações simultâneas escreviam no mesmo arquivo. Agora a leitura é compartilhada e a gravação é enfileirada, via arquivo temporário + rename. - POST /api/cache/sync: aceitava qualquer objeto e usava code como chave do cache (inclusive ausente ou não numérico); agora só grava produtos com code numérico e conta os demais em failed. JSON malformado responde 400 em vez de 500. - GET /api/products/[barcode]: resposta da API sem product virava 200 com corpo vazio; agora responde 404. - Chamadas ao Open Food Facts (busca e produto) passam a ter timeout de 15 s e o User-Agent vem de um único lugar (lib/offSearch).
This commit is contained in:
1 parent
5ee5dcd893
commit
30d7ef701f
7 files changed
+273
-30
No files matched your search
Vendored
+23
-9
@@ -1,26 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { jsonCacheManager } from "@/lib/jsonCacheManager";
|
||||
import type { Product } from "@/lib/openFoodFactsApi";
|
||||
|
||||
const INVALID_PRODUCTS = { error: "Produtos inválidos" };
|
||||
|
||||
/** Aceita só objetos com `code` numérico (texto de dígitos): a chave do cache não pode ser arbitrária. */
|
||||
function isSyncableProduct(value: unknown): value is Product {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const code = (value as { code?: unknown }).code;
|
||||
return typeof code === "string" && /^\d{1,32}$/.test(code);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
let body: { products?: unknown };
|
||||
try {
|
||||
const { products } = await request.json();
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json(INVALID_PRODUCTS, { status: 400 });
|
||||
}
|
||||
|
||||
if (!products || !Array.isArray(products)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Produtos inválidos" },
|
||||
{ status: 400 }
|
||||
);
|
||||
try {
|
||||
const products = body?.products;
|
||||
if (!Array.isArray(products)) {
|
||||
return NextResponse.json(INVALID_PRODUCTS, { status: 400 });
|
||||
}
|
||||
|
||||
// Salvar produtos no cache
|
||||
const result = await jsonCacheManager.saveProducts(products);
|
||||
// Salvar produtos válidos no cache; os demais contam como falha
|
||||
const valid = products.filter(isSyncableProduct);
|
||||
const result = await jsonCacheManager.saveProducts(valid);
|
||||
|
||||
console.log(`[Cache Sync] ${result.saved} novos produtos salvos`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
saved: result.saved,
|
||||
failed: result.failed,
|
||||
failed: result.failed + (products.length - valid.length),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erro ao sincronizar cache:", error);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { jsonCacheManager } from "@/lib/jsonCacheManager";
|
||||
|
||||
const USER_AGENT = "NutriScan/1.0 (https://nutriscan.com.br)";
|
||||
import { OFF_TIMEOUT_MS, OFF_USER_AGENT } from "@/lib/offSearch";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
@@ -55,9 +54,10 @@ export async function GET(
|
||||
`https://world.openfoodfacts.org/api/v2/product/${cleanBarcode}.json`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": USER_AGENT,
|
||||
"User-Agent": OFF_USER_AGENT,
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: AbortSignal.timeout(OFF_TIMEOUT_MS),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function GET(
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 0) {
|
||||
if (data.status === 0 || !data.product) {
|
||||
return NextResponse.json(
|
||||
{ error: "Produto não encontrado" },
|
||||
{ status: 404 }
|
||||
|
||||
Reference in new issue
Block a user