import { ClientRequestOptions } from '../types'; import { NextResponse } from 'next/server'; import settings from 'settings'; import logger from '../utils/log'; import formatCookieString from '../utils/format-cookie-string'; import cookieParser from 'set-cookie-parser'; import { cookies } from 'next/headers'; import getRootHostname from '../utils/get-root-hostname'; import { LocaleUrlStrategy } from '../localization'; import { fixtureManager, MockMode } from '../lib/fixture-manager'; interface RouteParams { params: { slug: string[]; }; } async function proxyRequest(...args) { const [req, routeContext] = args as [req: Request, { params: Promise }]; const params = await routeContext.params; const { searchParams } = new URL(req.url); const commerceUrl = settings.commerceUrl; if (commerceUrl === 'default') { return NextResponse.json( { error: 'Commerce URL not found' }, { status: 404 } ); } const options: ClientRequestOptions = { useTrailingSlash: true, useFormData: false, contentType: null, accept: 'application/json', responseType: 'json' }; const slug = `${params.slug.join('/')}/`; const options_ = JSON.parse( decodeURIComponent((searchParams.get('options') as string) ?? '{}') ); const urlSearchParams = new URLSearchParams(); Object.assign(options, options_); Array.from(searchParams.keys()) .filter((key) => !['slug', 'options'].includes(key)) .forEach((key) => { urlSearchParams.delete(key); searchParams.getAll(key).forEach((value) => { urlSearchParams.append(key, value); }); }); const extraHeaders: Record = {}; for (const [key, value] of Array.from(req.headers.entries())) { extraHeaders[key.toLowerCase()] = value; } const excludedHeaders = [ 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-requested-with', 'origin', 'referer', 'accept', 'content-length', 'content-type', 'host' ]; excludedHeaders.forEach((header) => { if (!settings.includedProxyHeaders?.includes(header)) { delete extraHeaders[header]; } }); const fetchOptions = { method: req.method, headers: { ...extraHeaders, 'X-Requested-With': 'XMLHttpRequest', Referer: commerceUrl, Accept: options.accept } } as RequestInit; const nextCookies = await cookies(); const segment = nextCookies.get('pz-segment')?.value; const currency = nextCookies.get('pz-external-currency')?.value; if (segment) { fetchOptions.headers['X-Segment-Id'] = segment; } if (currency) { fetchOptions.headers = Object.assign({}, fetchOptions.headers, { 'x-currency': currency }); } if (options.contentType) { fetchOptions.headers['Content-Type'] = options.contentType; } const isMultipartFormData = req.headers .get('content-type') ?.includes('multipart/form-data;'); if (req.method !== 'GET') { let body: Record | FormData = {}; try { body = isMultipartFormData ? await req.formData() : await req.json(); } catch (error) { logger.error( `Client Proxy Request - Error while parsing request body to JSON`, { url: req.url, error } ); } if (isMultipartFormData) { fetchOptions.body = body as FormData; } else { const formData = new FormData(); Object.keys(body ?? {}).forEach((key) => { if (body[key]) { if (typeof body[key] === 'object' && body[key] !== null) { formData.append(key, JSON.stringify(body[key])); } else { formData.append(key, body[key]); } } }); fetchOptions.body = !options.useFormData ? JSON.stringify(body) : formData; } } let url = `${commerceUrl}/${slug.replace(/,/g, '/')}`; if (!options.useTrailingSlash) { url = url.replace(/\/$/, ''); } if (urlSearchParams.toString().length) { url += `?${urlSearchParams.toString()}`; } const mockMode = process.env.PZ_MOCK; const fixtureBody = req.method !== 'GET' ? fetchOptions.body : undefined; // Replay mode: serve from fixtures if (mockMode === MockMode.REPLAY) { const { found, fixture } = await fixtureManager.read(req.method, slug, fixtureBody); if (found) { return NextResponse.json( options.responseType === 'text' ? { result: fixture.response.body } : fixture.response.body, { status: fixture.response.status } ); } return NextResponse.json( { error: 'No fixture recorded for this endpoint' }, { status: 404 } ); } try { const request = await fetch(url, fetchOptions); if (request.status === 204) { return new Response(null, { status: 204 }); } let response = {} as any; try { response = await (options.responseType === 'text' ? request.text() : request.json()); } catch (error) { logger.error( `Client Proxy Request - Error while parsing response body to ${options.responseType}`, { url, status: request.status, fetchOptions: JSON.stringify(fetchOptions), error } ); } // Record mode: save response to fixtures if (mockMode === MockMode.RECORD) { await fixtureManager.write(req.method, slug, fixtureBody, { status: request.status, headers: fixtureManager.extractHeaders(request.headers), body: response }); } const setCookieHeaders = request.headers.getSetCookie(); const exceptCookieKeys = ['pz-locale', 'pz-currency']; const isExcludedCookie = (name: string) => exceptCookieKeys.includes(name); const filteredCookies = cookieParser .parse(setCookieHeaders) .filter((cookie) => !isExcludedCookie(cookie.name)); const responseHeaders: any = {}; if (filteredCookies.length > 0) { const { localeUrlStrategy } = settings.localization; const fallbackHost = req.headers.get('x-forwarded-host') || req.headers.get('host'); const hostname = process.env.NEXT_PUBLIC_URL || `https://${fallbackHost}`; const rootHostname = localeUrlStrategy === LocaleUrlStrategy.Subdomain ? getRootHostname(hostname) : null; responseHeaders['set-cookie'] = filteredCookies .map((cookie) => { if (!cookie.domain && rootHostname) { cookie.domain = rootHostname; } return formatCookieString(cookie); }) .join(', '); } return NextResponse.json( options.responseType === 'text' ? { result: response } : response, { status: request.status, headers: responseHeaders } ); } catch (error) { logger.error('Client proxy request failed', error); return NextResponse.json({ error }, { status: 500 }); } } export async function GET(...args) { return proxyRequest(...args); } export async function POST(...args) { return proxyRequest(...args); } export async function PUT(...args) { return proxyRequest(...args); } export async function PATCH(...args) { return proxyRequest(...args); } export async function DELETE(...args) { return proxyRequest(...args); } export async function HEAD(...args) { return proxyRequest(...args); }