From b163e9f22ced1141d87777999344919cc44d513b Mon Sep 17 00:00:00 2001 From: John Dalmolin <82729254+john-dalmolin@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:10:47 -0300 Subject: [PATCH] fix: prefer https for public swagger urls --- src/docs/swagger.ts | 20 +++++++++++++++++++- tests/contracts/swagger.test.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index bce8755..47a3d93 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -25,10 +25,20 @@ const getForwardedHeaderValue = ( return value.split(",")[0]?.trim() || undefined; }; +const isLocalHost = (host: string): boolean => { + const normalizedHost = host.toLowerCase(); + + return ( + normalizedHost.startsWith("localhost") || + normalizedHost.startsWith("127.0.0.1") || + normalizedHost.startsWith("[::1]") + ); +}; + export const resolveSwaggerBaseUrl = ( request: SwaggerRequestLike, ): string => { - const protocol = + const requestedProtocol = getForwardedHeaderValue(request, "x-forwarded-proto") ?? request.protocol; const host = getForwardedHeaderValue(request, "x-forwarded-host") ?? request.get("host"); @@ -37,6 +47,14 @@ export const resolveSwaggerBaseUrl = ( return LOCAL_SWAGGER_SERVER.url; } + // Public proxy platforms can forward traffic to the container over plain HTTP + // even when the external URL is HTTPS. Prefer HTTPS for non-local hosts so + // Swagger "Try it out" targets the public origin instead of an internal hop. + const protocol = + requestedProtocol === "http" && !isLocalHost(host) + ? "https" + : requestedProtocol; + return `${protocol}://${host}`; }; diff --git a/tests/contracts/swagger.test.ts b/tests/contracts/swagger.test.ts index c874d78..bdbccc7 100644 --- a/tests/contracts/swagger.test.ts +++ b/tests/contracts/swagger.test.ts @@ -66,6 +66,19 @@ describe("swagger spec", () => { expect(baseUrl).toBe("https://auth-api-production-a97b.up.railway.app"); }); + it("prefers https for non-local public hosts when the proxy reports http", () => { + const baseUrl = resolveSwaggerBaseUrl({ + protocol: "http", + get(header: string) { + return header === "host" + ? "auth-api-production-a97b.up.railway.app" + : undefined; + }, + }); + + expect(baseUrl).toBe("https://auth-api-production-a97b.up.railway.app"); + }); + it("falls back to the local server when the host header is unavailable", () => { const baseUrl = resolveSwaggerBaseUrl({ protocol: "https",