mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-12 01:36:43 +02:00
fix(produits): recherche asynchrone — la requête était coupée par le reverse proxy
« Unexpected token '<', "<html> <h"... is not valid JSON » : ce n'était pas du JSON parce que ce n'était pas l'application qui répondait. La recherche enchaîne deux allers-retours Qlik (dont une extraction mensuelle) et dépassait le délai du proxy, qui renvoyait sa page d'erreur HTML. - L'API passe en asynchrone, sur le modèle déjà éprouvé de POST /api/qlik/sync : POST démarre un job et rend la main immédiatement, GET renvoie l'avancement puis le résultat. Le client interroge toutes les 2 s et affiche l'étape en cours (recherche des articles / extraction des ventes / rapprochement catalogue). Deux recherches simultanées au maximum. - Le client ne présume plus que la réponse est du JSON : les statuts 504, 502, 503, 401 et 403 sont traduits en message actionnable au lieu d'une erreur de parsing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
4 files changed
+295
-66
No files matched your search
@@ -1,43 +1,70 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { rechercherProduits } from "@/features/produits/api/search-produits";
|
||||
import { demarrerRecherche, etatRecherche } from "@/features/produits/api/search-produits";
|
||||
|
||||
// L'extraction Qlik (Playwright + Engine) prend plusieurs secondes, parfois
|
||||
// plus d'une minute quand le serveur Qlik est chargé.
|
||||
export const maxDuration = 300;
|
||||
// Playwright/Chromium exige le runtime Node.js (pas edge).
|
||||
// Les réponses sont immédiates (le travail Qlik tourne en tâche de fond), mais on
|
||||
// garde le runtime Node.js : Playwright/Chromium ne tourne pas en edge, et l'état
|
||||
// des jobs vit dans la mémoire du process.
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/produits/search?q=<terme>[&force=1]
|
||||
* Recherche produit **Qlik d'abord** — API asynchrone.
|
||||
*
|
||||
* Recherche produit **Qlik d'abord** : les articles du réseau qui correspondent
|
||||
* au terme, leurs métriques réseau sur 12 mois glissants, puis le rapprochement
|
||||
* avec le catalogue FF Nancy.
|
||||
* `POST /api/produits/search?q=<terme>[&force=1]` démarre la recherche et rend la
|
||||
* main tout de suite. `GET /api/produits/search?q=<terme>` renvoie l'avancement,
|
||||
* puis le résultat.
|
||||
*
|
||||
* `force=1` ignore le cache mémoire de 10 min (bouton « relancer »).
|
||||
* Pourquoi asynchrone : une recherche enchaîne deux allers-retours Qlik (dont une
|
||||
* extraction mensuelle) et peut dépasser la minute. Une requête HTTP maintenue
|
||||
* aussi longtemps se fait couper par le reverse proxy, qui répond une page
|
||||
* d'erreur **HTML** — le client échouait alors sur « Unexpected token '<' … is
|
||||
* not valid JSON ». Même schéma que `POST /api/qlik/sync`.
|
||||
*
|
||||
* Le middleware Next ne protège pas `/api/*` : contrôle de session explicite.
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
|
||||
async function requireSession(): Promise<NextResponse | null> {
|
||||
const session = await auth();
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
return null;
|
||||
}
|
||||
|
||||
function lireTerme(req: NextRequest): string | null {
|
||||
const q = (req.nextUrl.searchParams.get("q") ?? "").trim();
|
||||
if (q.length < 3) {
|
||||
return NextResponse.json({ error: "Saisissez au moins 3 caractères" }, { status: 400 });
|
||||
}
|
||||
const force = req.nextUrl.searchParams.get("force") === "1";
|
||||
return q.length >= 3 ? q : null;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const denied = await requireSession();
|
||||
if (denied) return denied;
|
||||
|
||||
const q = lireTerme(req);
|
||||
if (!q) return NextResponse.json({ error: "Saisissez au moins 3 caractères" }, { status: 400 });
|
||||
|
||||
const force = req.nextUrl.searchParams.get("force") === "1";
|
||||
try {
|
||||
const result = await rechercherProduits(q, { force });
|
||||
return NextResponse.json({ success: true, ...result });
|
||||
const job = demarrerRecherche(q, { force });
|
||||
return NextResponse.json({ success: true, ...job });
|
||||
} catch (e) {
|
||||
const message = (e as Error).message ?? String(e);
|
||||
console.error("[api/produits/search]", message.slice(0, 300));
|
||||
console.error("[api/produits/search] POST", message.slice(0, 300));
|
||||
return NextResponse.json({ error: message.slice(0, 300) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireSession();
|
||||
if (denied) return denied;
|
||||
|
||||
const q = lireTerme(req);
|
||||
if (!q) return NextResponse.json({ error: "Saisissez au moins 3 caractères" }, { status: 400 });
|
||||
|
||||
try {
|
||||
return NextResponse.json({ success: true, ...etatRecherche(q) });
|
||||
} catch (e) {
|
||||
const message = (e as Error).message ?? String(e);
|
||||
console.error("[api/produits/search] GET", message.slice(0, 300));
|
||||
return NextResponse.json({ error: message.slice(0, 300) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user