accounts/dashboard/lib/apiProxy.ts

108 lines
3.2 KiB
TypeScript

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const DEFAULT_FORWARD_HEADERS = [
'accept',
'accept-language',
'authorization',
'content-type',
'cookie',
'user-agent',
'x-account-session',
'x-forwarded-for',
'x-request-id',
'x-trace-id',
] as const
const BODYLESS_METHODS = new Set(['GET', 'HEAD'])
type ProxyOptions = {
upstreamBaseUrl: string
upstreamPathPrefix: string
allowedHeaders?: readonly string[]
}
function stripTrailingSlash(value: string): string {
return value.endsWith('/') ? value.slice(0, -1) : value
}
function buildTargetUrl(request: NextRequest, { upstreamBaseUrl, upstreamPathPrefix }: ProxyOptions): string {
const normalizedBase = stripTrailingSlash(upstreamBaseUrl)
const normalizedPrefix = upstreamPathPrefix.startsWith('/') ? upstreamPathPrefix : `/${upstreamPathPrefix}`
const suffix = request.nextUrl.pathname.slice(normalizedPrefix.length)
const normalizedSuffix = suffix.startsWith('/') ? suffix : suffix ? `/${suffix}` : ''
const search = request.nextUrl.search ?? ''
return `${normalizedBase}${normalizedPrefix}${normalizedSuffix}${search}`
}
function buildForwardHeaders(request: NextRequest, allowedHeaders: readonly string[] = DEFAULT_FORWARD_HEADERS) {
const headers = new Headers()
for (const name of allowedHeaders) {
const value = request.headers.get(name)
if (value) {
headers.set(name, value)
}
}
return headers
}
function applySetCookieHeaders(source: Headers, target: Headers) {
const getSetCookie = (source as Headers & { getSetCookie?: () => string[] }).getSetCookie
if (typeof getSetCookie === 'function') {
for (const cookie of getSetCookie.call(source)) {
target.append('set-cookie', cookie)
}
return
}
const cookie = source.get('set-cookie')
if (cookie) {
target.append('set-cookie', cookie)
}
}
export async function proxyRequestToUpstream(request: NextRequest, options: ProxyOptions): Promise<Response> {
const targetUrl = buildTargetUrl(request, options)
const forwardHeaders = buildForwardHeaders(request, options.allowedHeaders)
let body: ArrayBuffer | undefined
if (!BODYLESS_METHODS.has(request.method.toUpperCase())) {
body = await request.arrayBuffer()
}
let upstreamResponse: Response
try {
upstreamResponse = await fetch(targetUrl, {
method: request.method,
headers: forwardHeaders,
body: body ? Buffer.from(body) : undefined,
cache: 'no-store',
redirect: 'manual',
})
} catch (error) {
console.error('Proxy request failed', error)
return NextResponse.json({ error: 'upstream_unreachable' }, { status: 502 })
}
const responseHeaders = new Headers()
upstreamResponse.headers.forEach((value, key) => {
if (key.toLowerCase() === 'set-cookie') {
return
}
responseHeaders.set(key, value)
})
applySetCookieHeaders(upstreamResponse.headers, responseHeaders)
responseHeaders.set('Cache-Control', 'no-store')
return new Response(upstreamResponse.body, {
status: upstreamResponse.status,
headers: responseHeaders,
})
}
export function createUpstreamProxyHandler(options: ProxyOptions) {
return function handler(request: NextRequest) {
return proxyRequestToUpstream(request, options)
}
}