diff --git a/src/_utils.ts b/src/_utils.ts index b7989be..9700f26 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -99,8 +99,7 @@ export function setupOutgoing(outgoing, options, req, forward?) { // you are doing and are using conflicting options. // outgoingPath = options.ignorePath ? "" : outgoingPath; - - outgoing.path = urlJoin(targetPath, outgoingPath); + outgoing.path = joinURL(targetPath, outgoingPath); if (options.changeOrigin) { outgoing.headers.host = @@ -112,6 +111,29 @@ export function setupOutgoing(outgoing, options, req, forward?) { return outgoing; } +// From https://github.com/unjs/h3/blob/e8adfa/src/utils/internal/path.ts#L16C1-L36C2 +export function joinURL( + base: string | undefined, + path: string | undefined, +): string { + if (!base || base === "/") { + return path || "/"; + } + if (!path || path === "/") { + return base || "/"; + } + // eslint-disable-next-line unicorn/prefer-at + const baseHasTrailing = base[base.length - 1] === "/"; + const pathHasLeading = path[0] === "/"; + if (baseHasTrailing && pathHasLeading) { + return base + path.slice(1); + } + if (!baseHasTrailing && !pathHasLeading) { + return base + "/" + path; + } + return base + path; +} + /** * Set the proper configuration for sockets, * set no delay and set keep alive, also set @@ -168,44 +190,6 @@ export function hasEncryptedConnection(req) { return Boolean(req.connection.encrypted || req.connection.pair); } -/** - * OS-agnostic join (doesn't break on URLs like path.join does on Windows)> - * - * @return {String} The generated path. - * - * @api private - */ - -export function urlJoin(...args: string[]) { - // We do not want to mess with the query string. All we want to touch is the path. - const lastIndex = args.length - 1; - const last = args[lastIndex]; - const lastSegs = last.split("?"); - - args[lastIndex] = lastSegs.shift(); - - // - // Join all strings, but remove empty strings so we don't get extra slashes from - // joining e.g. ['', 'am'] - // - const retSegs = [ - args - .filter(Boolean) - .join("/") - .replace(/\/+/g, "/") - .replace("http:/", "http://") - .replace("https:/", "https://"), - ]; - - // Only join the query string if it exists so we don't have trailing a '?' - // on every request - - // Handle case where there could be multiple ? in the URL. - retSegs.push(...lastSegs); - - return retSegs.join("?"); -} - /** * Rewrites or removes the domain of a cookie header * diff --git a/test/index.test.ts b/test/index.test.ts index 83e736f..6f88dc2 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,7 @@ -import { expect, it, describe, beforeAll } from "vitest"; +import { expect, it, describe } from "vitest"; import { listen, Listener } from "listhen"; import { $fetch } from "ofetch"; -import { createProxyServer, ProxyServer } from "../src"; +import { createProxyServer, ProxyServer, ProxyServerOptions } from "../src"; describe("httpxy", () => { let mainListener: Listener; @@ -11,7 +11,12 @@ describe("httpxy", () => { let lastResolved: any; let lastRejected: any; - beforeAll(async () => { + const maskResponse = (obj: any) => ({ + ...obj, + headers: { ...obj.headers, connection: "<>", host: "<>" }, + }); + + const makeProxy = async (options: ProxyServerOptions) => { mainListener = await listen((req, res) => { res.end( JSON.stringify({ @@ -22,7 +27,7 @@ describe("httpxy", () => { ); }); - proxy = createProxyServer({}); + proxy = createProxyServer(options); proxyListener = await listen(async (req, res) => { lastResolved = false; @@ -36,17 +41,13 @@ describe("httpxy", () => { res.end("Proxy error: " + error.toString()); } }); - }); + }; it("works", async () => { + await makeProxy({}); const mainResponse = await $fetch(mainListener.url + "base/test?foo"); const proxyResponse = await $fetch(proxyListener.url + "test?foo"); - const maskResponse = (obj: any) => ({ - ...obj, - headers: { ...obj.headers, connection: "<>", host: "<>" }, - }); - expect(maskResponse(await mainResponse)).toMatchObject( maskResponse(proxyResponse), ); @@ -56,4 +57,18 @@ describe("httpxy", () => { expect(lastResolved).toBe(true); expect(lastRejected).toBe(undefined); }); + + it("should avoid normalize url", async () => { + const mainResponse = await $fetch(mainListener.url + "base/a/b//c"); + const proxyResponse = await $fetch(proxyListener.url + "a/b//c"); + + expect(maskResponse(await mainResponse)).toMatchObject( + maskResponse(proxyResponse), + ); + + expect(proxyResponse.path).toBe("/base/a/b//c"); + + expect(lastResolved).toBe(true); + expect(lastRejected).toBe(undefined); + }); });