From 0ca2846f2b484d6b4a8006ba62ca25d55265f6ba Mon Sep 17 00:00:00 2001 From: gilles Date: Tue, 22 Sep 2026 09:46:24 +0200 Subject: [PATCH] =?UTF-8?q?fix(ssr):=20r=C3=A9soudre=20une=20base=20absolu?= =?UTF-8?q?e=20pour=20les=20fetch=20API=20c=C3=B4t=C3=A9=20serveur=20(page?= =?UTF-8?q?=20=C3=A9dition)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 +++ src/lib/api-client.ssr.test.ts | 48 ++++++++++++++++++++++++++++++++++ src/lib/api-client.ts | 8 +++++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/lib/api-client.ssr.test.ts diff --git a/.env.example b/.env.example index 67c4feb..abe1636 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ # Variables exposées au client (préfixées NEXT_PUBLIC_) # NEXT_PUBLIC_API_URL=http://localhost:3000 +# Base interne pour les fetch SSR (port interne du serveur Next.js, défaut 3000) +# API_BASE_URL=http://localhost:3000 + # Implémentation repository active : mysql | mongo DB_IMPL=mysql diff --git a/src/lib/api-client.ssr.test.ts b/src/lib/api-client.ssr.test.ts new file mode 100644 index 0000000..a951fa4 --- /dev/null +++ b/src/lib/api-client.ssr.test.ts @@ -0,0 +1,48 @@ +// @vitest-environment node +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const fetchMock = vi.fn() + +async function loadModule(env?: Record) { + vi.resetModules() + delete process.env.API_BASE_URL + if (env) Object.assign(process.env, env) + return (await import('./api-client')) as typeof import('./api-client') +} + +beforeEach(() => { + global.fetch = fetchMock as unknown as typeof fetch + fetchMock.mockReset() +}) + +describe('cardsApi (SSR / Node)', () => { + it("résout une URL absolue sur localhost:3000 en l'absence de window", async () => { + const { cardsApi: api } = await loadModule() + fetchMock.mockResolvedValue( + new Response(JSON.stringify([]), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ) + await api.list() + expect(fetchMock).toHaveBeenCalledWith( + 'http://localhost:3000/api/cards', + expect.objectContaining({ method: 'GET' }), + ) + }) + + it('respecte API_BASE_URL si elle est définie', async () => { + const { cardsApi: api } = await loadModule({ API_BASE_URL: 'http://api:9999' }) + fetchMock.mockResolvedValue( + new Response(JSON.stringify([]), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ) + await api.list() + expect(fetchMock).toHaveBeenCalledWith( + 'http://api:9999/api/cards', + expect.objectContaining({ method: 'GET' }), + ) + }) +}) diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 2860cc4..3b65199 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -14,10 +14,16 @@ interface ApiErrorPayload { message?: string } +// Côté serveur (SSR/standalone), une URL relative n'est pas résolvable : +// on cible la propre API de l'application (port interne 3000). Côté client, +// l'URL relative est résolue par le navigateur. +const API_BASE_URL = + typeof window === 'undefined' ? (process.env.API_BASE_URL ?? 'http://localhost:3000') : '' + async function request(path: string, init?: RequestInit): Promise { let res: Response try { - res = await fetch(path, { + res = await fetch(`${API_BASE_URL}${path}`, { method: init?.method ?? 'GET', ...init, headers: { 'Content-Type': 'application/json', ...init?.headers },