Skip to content

Commit 302f9a3

Browse files
authored
fix(cli): Validate regionUrl is absolute to prevent URL parsing errors
1 parent bd06508 commit 302f9a3

2 files changed

Lines changed: 88 additions & 1 deletion

File tree

packages/cli/src/lib/region.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getConfiguredSentryUrl } from "./constants.js";
1010
import { getOrgByNumericId, getOrgRegion, setOrgRegion } from "./db/regions.js";
1111
import { stripDsnOrgPrefix } from "./dsn/index.js";
1212
import { withAuthGuard } from "./errors.js";
13+
import { logger } from "./logger.js";
1314
import { getSdkConfig } from "./sentry-client.js";
1415
import { getSentryBaseUrl, isSentrySaasUrl } from "./sentry-urls.js";
1516

@@ -85,7 +86,23 @@ async function resolveOrgRegionUncached(orgSlug: string): Promise<string> {
8586
throw response.error;
8687
}
8788

88-
const regionUrl = response.data?.links?.regionUrl || baseUrl;
89+
// Validate that the regionUrl is an absolute URL. Self-hosted instances
90+
// may return a relative path (e.g. "/") which is truthy but would break
91+
// fetch calls that depend on an absolute base URL.
92+
const rawRegionUrl = response.data?.links?.regionUrl;
93+
let regionUrl: string;
94+
try {
95+
if (rawRegionUrl && new URL(rawRegionUrl).hostname) {
96+
regionUrl = rawRegionUrl;
97+
} else {
98+
regionUrl = baseUrl;
99+
}
100+
} catch {
101+
logger.debug(
102+
`regionUrl "${rawRegionUrl}" from API is not a valid absolute URL; falling back to baseUrl`
103+
);
104+
regionUrl = baseUrl;
105+
}
89106

90107
// Cache for future use. setOrgRegion also extends the in-process
91108
// trust class so the subsequent request to this region passes the

packages/cli/test/lib/region.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,76 @@ describe("resolveOrgRegion", () => {
203203
}
204204
});
205205

206+
test("falls back to baseUrl when API returns a relative regionUrl", async () => {
207+
const originalFetch = globalThis.fetch;
208+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
209+
const req = new Request(input, init);
210+
if (req.url.includes("/organizations/relative-region-org/")) {
211+
return new Response(
212+
JSON.stringify({
213+
id: "789",
214+
slug: "relative-region-org",
215+
name: "Self-hosted Org",
216+
links: {
217+
organizationUrl: "/organizations/relative-region-org/",
218+
// Self-hosted instance returns a relative path instead of an absolute URL
219+
regionUrl: "/",
220+
},
221+
}),
222+
{
223+
status: 200,
224+
headers: { "Content-Type": "application/json" },
225+
}
226+
);
227+
}
228+
return new Response(JSON.stringify({ detail: "Not found" }), {
229+
status: 404,
230+
});
231+
};
232+
233+
try {
234+
const regionUrl = await resolveOrgRegion("relative-region-org");
235+
// Should fall back to the configured baseUrl, not use the relative regionUrl
236+
expect(regionUrl).toBe("https://sentry.io");
237+
} finally {
238+
globalThis.fetch = originalFetch;
239+
}
240+
});
241+
242+
test("falls back to baseUrl when API returns a malformed regionUrl", async () => {
243+
const originalFetch = globalThis.fetch;
244+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
245+
const req = new Request(input, init);
246+
if (req.url.includes("/organizations/malformed-region-org/")) {
247+
return new Response(
248+
JSON.stringify({
249+
id: "790",
250+
slug: "malformed-region-org",
251+
name: "Self-hosted Org 2",
252+
links: {
253+
organizationUrl: "/organizations/malformed-region-org/",
254+
regionUrl: "not-a-valid-url",
255+
},
256+
}),
257+
{
258+
status: 200,
259+
headers: { "Content-Type": "application/json" },
260+
}
261+
);
262+
}
263+
return new Response(JSON.stringify({ detail: "Not found" }), {
264+
status: 404,
265+
});
266+
};
267+
268+
try {
269+
const regionUrl = await resolveOrgRegion("malformed-region-org");
270+
expect(regionUrl).toBe("https://sentry.io");
271+
} finally {
272+
globalThis.fetch = originalFetch;
273+
}
274+
});
275+
206276
test("falls back to default URL when API call fails", async () => {
207277
// Mock fetch to fail
208278
const originalFetch = globalThis.fetch;

0 commit comments

Comments
 (0)