diff --git a/api/_proxy.js b/api/_proxy.js index c1a1fd9..1baf203 100644 --- a/api/_proxy.js +++ b/api/_proxy.js @@ -28,7 +28,7 @@ function buildHeaders(sourceHeaders) { return headers; } -function requireHttpsTarget(envName) { +function requireProxyTarget(envName) { const rawTarget = process.env[envName]?.trim(); if (!rawTarget) { @@ -37,18 +37,40 @@ function requireHttpsTarget(envName) { const target = new URL(rawTarget); - if (target.protocol !== 'https:') { - throw new Error(`${envName} must use an https URL.`); + if (!['http:', 'https:'].includes(target.protocol)) { + throw new Error(`${envName} must use an http or https URL.`); } return target; } -function buildTargetUrl(requestUrl, prefix, targetBase) { +function normalizeProxyPrefixes({ prefix, prefixes }) { + if (Array.isArray(prefixes)) { + return prefixes; + } + + if (prefix) { + return [prefix]; + } + + return []; +} + +function stripProxyPrefix(pathname, prefixes) { + const matchedPrefix = prefixes.find((prefix) => { + return pathname === prefix || pathname.startsWith(`${prefix}/`); + }); + + if (!matchedPrefix) { + return pathname; + } + + return pathname.slice(matchedPrefix.length) || '/'; +} + +function buildTargetUrl(requestUrl, options, targetBase) { const incomingUrl = new URL(requestUrl, 'https://vercel.local'); - const pathname = incomingUrl.pathname.startsWith(prefix) - ? incomingUrl.pathname.slice(prefix.length) - : incomingUrl.pathname; + const pathname = stripProxyPrefix(incomingUrl.pathname, normalizeProxyPrefixes(options)); const targetUrl = new URL(pathname || '/', targetBase); targetUrl.search = incomingUrl.search; @@ -56,11 +78,11 @@ function buildTargetUrl(requestUrl, prefix, targetBase) { return targetUrl; } -async function proxyRequest(request, response, { envName, prefix }) { +async function proxyRequest(request, response, { envName, prefix, prefixes }) { let targetUrl; try { - targetUrl = buildTargetUrl(request.url, prefix, requireHttpsTarget(envName)); + targetUrl = buildTargetUrl(request.url, { prefix, prefixes }, requireProxyTarget(envName)); } catch (error) { response.statusCode = 500; response.setHeader('content-type', 'application/json; charset=utf-8'); @@ -110,5 +132,6 @@ async function proxyRequest(request, response, { envName, prefix }) { } module.exports = { + buildTargetUrl, proxyRequest, }; diff --git a/api/_proxy.test.js b/api/_proxy.test.js new file mode 100644 index 0000000..b1e8cd8 --- /dev/null +++ b/api/_proxy.test.js @@ -0,0 +1,46 @@ +import { createRequire } from 'node:module'; + +import { describe, expect, it } from 'vitest'; + +const require = createRequire(import.meta.url); +const { buildTargetUrl } = require('./_proxy'); + +describe('buildTargetUrl', () => { + const targetBase = new URL('https://backend.example.com'); + + it('strips the rewritten Vercel function prefix', () => { + const targetUrl = buildTargetUrl( + '/api/be1/api/v1/scan?limit=3', + { + prefixes: ['/api/be1', '/be1'], + }, + targetBase, + ); + + expect(targetUrl.toString()).toBe('https://backend.example.com/api/v1/scan?limit=3'); + }); + + it('strips the original public proxy prefix', () => { + const targetUrl = buildTargetUrl( + '/be1/api/v1/scan', + { + prefixes: ['/api/be1', '/be1'], + }, + targetBase, + ); + + expect(targetUrl.toString()).toBe('https://backend.example.com/api/v1/scan'); + }); + + it('does not partially strip similarly named paths', () => { + const targetUrl = buildTargetUrl( + '/be10/api/v1/scan', + { + prefixes: ['/be1'], + }, + targetBase, + ); + + expect(targetUrl.toString()).toBe('https://backend.example.com/be10/api/v1/scan'); + }); +}); diff --git a/api/be1/[...path].js b/api/be1/[...path].js index 52840bb..ebf735a 100644 --- a/api/be1/[...path].js +++ b/api/be1/[...path].js @@ -3,6 +3,6 @@ const { proxyRequest } = require('../_proxy'); module.exports = function handler(request, response) { return proxyRequest(request, response, { envName: 'BE1_PROXY_TARGET_URL', - prefix: '/api/be1', + prefixes: ['/api/be1', '/be1'], }); }; diff --git a/api/be3/[...path].js b/api/be3/[...path].js index 20795ed..f4864ec 100644 --- a/api/be3/[...path].js +++ b/api/be3/[...path].js @@ -3,6 +3,6 @@ const { proxyRequest } = require('../_proxy'); module.exports = function handler(request, response) { return proxyRequest(request, response, { envName: 'BE3_PROXY_TARGET_URL', - prefix: '/api/be3', + prefixes: ['/api/be3', '/be3'], }); };