156 lines
No EOL
4.7 KiB
TypeScript
156 lines
No EOL
4.7 KiB
TypeScript
|
|
|
|
// lib/upstream.ts
|
|
// Helper pour proxy côté serveur vers l'API upstream (API Gateway/Lambda).
|
|
// Objectif : être résilient aux variantes de chemin (avec /api ou sans),
|
|
// centraliser l'URL de base et uniformiser les appels.
|
|
//
|
|
// Variables d'environnement attendues (côté SERVEUR, non NEXT_PUBLIC) :
|
|
// - UPSTREAM_API_BASE ex: https://xxxx.execute-api.eu-west-3.amazonaws.com/default
|
|
// - UPSTREAM_API_PREFIX ex: "" (par défaut) ou "/api" pour préférer une des variantes
|
|
//
|
|
// Option debug : DEBUG_UPSTREAM=1 (en dev) pour logger les URLs tentées.
|
|
|
|
const BASE = process.env.UPSTREAM_API_BASE ?? "";
|
|
const PREFERRED_PREFIX = process.env.UPSTREAM_API_PREFIX ?? "";
|
|
const DEBUG_UPSTREAM =
|
|
process.env.NODE_ENV !== "production" && process.env.DEBUG_UPSTREAM === "1";
|
|
|
|
export type UpstreamFetchOpts = {
|
|
method?: string;
|
|
headers?: HeadersInit;
|
|
// Querystring à forwarder (ex: { year: "2025", page: "1" })
|
|
searchParams?: URLSearchParams | Record<string, string | number | undefined>;
|
|
// Corps brut (JSON stringifié par l'appelant si nécessaire)
|
|
body?: BodyInit | null;
|
|
};
|
|
|
|
/**
|
|
* Assure un leading slash unique ("/x" plutôt que "x" ou "//x")
|
|
*/
|
|
function withLeadingSlash(path: string): string {
|
|
if (!path) return "/";
|
|
return path.startsWith("/") ? path.replace(/\/{2,}/g, "/") : `/${path}`;
|
|
}
|
|
|
|
/**
|
|
* Construit une URL absolue à partir d'une base et d'un chemin relatif.
|
|
* - Normalise les "/" doublons
|
|
* - Ajoute les query params si fournis
|
|
*/
|
|
function buildUrl(
|
|
base: string,
|
|
path: string,
|
|
query?: UpstreamFetchOpts["searchParams"]
|
|
): string {
|
|
const baseNormalized = base.endsWith("/") ? base : base + "/";
|
|
// On enlève le premier "/" pour laisser URL gérer correctement la concaténation
|
|
const relative = withLeadingSlash(path).replace(/^\//, "");
|
|
const url = new URL(relative, baseNormalized);
|
|
|
|
if (query) {
|
|
const usp =
|
|
query instanceof URLSearchParams
|
|
? query
|
|
: new URLSearchParams(
|
|
Object.entries(query).flatMap(([k, v]) =>
|
|
v === undefined ? [] : [[k, String(v)]]
|
|
)
|
|
);
|
|
usp.forEach((v, k) => url.searchParams.set(k, v));
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
/**
|
|
* fetchUpstream: tente d'abord la variante préférée (UPSTREAM_API_PREFIX),
|
|
* puis retombe sur l'autre (/api ↔︎ "") si 404.
|
|
* Pour toute autre erreur (401, 403, 500...), on remonte la réponse immédiatement.
|
|
*/
|
|
export async function fetchUpstream(
|
|
path: string,
|
|
opts: UpstreamFetchOpts = {}
|
|
): Promise<Response> {
|
|
if (!BASE) {
|
|
throw new Error(
|
|
"UPSTREAM_API_BASE manquant. Définis la variable d'environnement côté serveur."
|
|
);
|
|
}
|
|
|
|
// Ordre de préférence pour les préfixes
|
|
const prefixes =
|
|
PREFERRED_PREFIX === "/api"
|
|
? ["/api", ""]
|
|
: PREFERRED_PREFIX === ""
|
|
? ["", "/api"]
|
|
: [PREFERRED_PREFIX, PREFERRED_PREFIX === "/api" ? "" : "/api"];
|
|
|
|
const attempts = prefixes.map((p) =>
|
|
p ? withLeadingSlash(p) + withLeadingSlash(path) : withLeadingSlash(path)
|
|
);
|
|
|
|
let last404: Response | undefined;
|
|
|
|
for (const candidate of attempts) {
|
|
const url = buildUrl(BASE, candidate, opts.searchParams);
|
|
|
|
if (DEBUG_UPSTREAM) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("UPSTREAM TRY:", url);
|
|
}
|
|
|
|
const resp = await fetch(url, {
|
|
method: opts.method ?? "GET",
|
|
headers: opts.headers,
|
|
body: opts.body ?? null,
|
|
cache: "no-store",
|
|
// Important quand l'upstream est externe : ne jamais envoyer automatiquement des cookies serveur
|
|
// (l'upstream ne les comprendrait pas). On ne met pas credentials ici.
|
|
});
|
|
|
|
if (resp.ok) return resp;
|
|
|
|
if (resp.status === 404) {
|
|
last404 = resp;
|
|
continue; // essaie l'autre variante
|
|
}
|
|
|
|
// Pour les autres codes, on renvoie directement
|
|
return resp;
|
|
}
|
|
|
|
// Toutes les tentatives ont renvoyé 404
|
|
if (last404) return last404;
|
|
|
|
// Sécurité : ne devrait pas arriver
|
|
return new Response("Not Found", { status: 404 });
|
|
}
|
|
|
|
/**
|
|
* Helper pratique : récupère le JSON directement, avec message d'erreur enrichi.
|
|
* Laisse remonter les codes HTTP. À utiliser dans les route handlers Next.
|
|
*/
|
|
export async function upstreamJson<T = unknown>(
|
|
path: string,
|
|
opts: UpstreamFetchOpts = {}
|
|
): Promise<{ data: T; response: Response }> {
|
|
const resp = await fetchUpstream(path, opts);
|
|
const text = await resp.text();
|
|
|
|
if (!resp.ok) {
|
|
// On renvoie quand même le body texte pour faciliter le debug côté route
|
|
throw new Error(
|
|
`Upstream error ${resp.status} on ${path}: ${text.slice(0, 500)}`
|
|
);
|
|
}
|
|
|
|
try {
|
|
const data = text ? (JSON.parse(text) as T) : ({} as T);
|
|
return { data, response: resp };
|
|
} catch {
|
|
throw new Error(
|
|
`Upstream JSON parse error on ${path}: ${text.slice(0, 500)}`
|
|
);
|
|
}
|
|
} |