From 81334ef7a6336649be8a4adf2b14e06c77e8de62 Mon Sep 17 00:00:00 2001 From: kimsubsub Date: Fri, 24 Apr 2026 19:29:34 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/_proxy.js | 41 ++++++++++++++++++++++++++++++--------- api/_proxy.test.js | 46 ++++++++++++++++++++++++++++++++++++++++++++ api/be1/[...path].js | 2 +- api/be3/[...path].js | 2 +- 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 api/_proxy.test.js 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'], }); };