From d50056d7c78a7504542e6afe15e94350af1ccf78 Mon Sep 17 00:00:00 2001 From: Claudear <262350598+claudear@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:51:44 +0000 Subject: [PATCH] fix: route projects in any cloud region to their subdomain (DISCORD-15265968) Clicking a project in the organization view got stuck for projects hosted in an Appwrite Cloud region that was not part of the hardcoded region list. getRegionSubdomain() resolved a project's regional subdomain via a switch that only knew fra/syd/nyc/sfo/sgp/tor. Any other region fell through to the empty default, so buildRegionalV1Endpoint() left the host unchanged and the SDK issued requests to the apex host (or the region baked into APPWRITE_ENDPOINT) instead of the project's real regional host (e.g. blr.cloud.appwrite.io). Those cross-origin requests failed CORS and the navigation hung. The impersonation tooling targets the correct regional endpoint directly, which is why the same action worked there. Derive the subdomain directly from the region id so any current or future region is routed correctly, keeping the `default` placeholder (and empty values) mapped to the apex domain. stripLeadingRegionSubdomain() now also strips the region about to be prepended, avoiding a doubled subdomain when the console itself is served from that region. Adds unit tests covering the regression and the edge cases. Co-Authored-By: Claude Opus 4.8 --- src/lib/helpers/apiEndpoint.test.ts | 83 +++++++++++++++++++++++++++++ src/lib/helpers/apiEndpoint.ts | 54 +++++++++---------- 2 files changed, 110 insertions(+), 27 deletions(-) create mode 100644 src/lib/helpers/apiEndpoint.test.ts diff --git a/src/lib/helpers/apiEndpoint.test.ts b/src/lib/helpers/apiEndpoint.test.ts new file mode 100644 index 0000000000..cfd8fd296d --- /dev/null +++ b/src/lib/helpers/apiEndpoint.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest'; +import { + buildRegionalV1Endpoint, + getRegionSubdomain, + stripLeadingRegionSubdomain +} from '$lib/helpers/apiEndpoint'; + +describe('getRegionSubdomain', () => { + it('maps the well-known regions to their subdomains', () => { + expect(getRegionSubdomain('fra')).toBe('fra.'); + expect(getRegionSubdomain('syd')).toBe('syd.'); + expect(getRegionSubdomain('nyc')).toBe('nyc.'); + expect(getRegionSubdomain('sfo')).toBe('sfo.'); + expect(getRegionSubdomain('sgp')).toBe('sgp.'); + expect(getRegionSubdomain('tor')).toBe('tor.'); + }); + + it('derives a subdomain for regions that are not hardcoded', () => { + // A project living in a newer Appwrite Cloud region (e.g. Bangalore) + // must still be routed to its own regional subdomain. Previously these + // fell through to the empty default, so the SDK hit the apex domain and + // navigation got stuck with CORS errors. + expect(getRegionSubdomain('blr')).toBe('blr.'); + expect(getRegionSubdomain('lon')).toBe('lon.'); + }); + + it('resolves the default placeholder and empty values to the apex domain', () => { + expect(getRegionSubdomain('default')).toBe(''); + expect(getRegionSubdomain('')).toBe(''); + expect(getRegionSubdomain(undefined)).toBe(''); + }); +}); + +describe('buildRegionalV1Endpoint', () => { + it('leaves the host untouched when multi-region is disabled', () => { + expect(buildRegionalV1Endpoint('https:', 'cloud.appwrite.io', 'blr', false)).toBe( + 'https://cloud.appwrite.io/v1' + ); + }); + + it('leaves the host untouched for the default/unknown placeholder region', () => { + expect(buildRegionalV1Endpoint('https:', 'fra.cloud.appwrite.io', 'default', true)).toBe( + 'https://fra.cloud.appwrite.io/v1' + ); + }); + + it('prepends the regional subdomain for a hardcoded region', () => { + expect(buildRegionalV1Endpoint('https:', 'fra.cloud.appwrite.io', 'nyc', true)).toBe( + 'https://nyc.cloud.appwrite.io/v1' + ); + }); + + it('prepends the regional subdomain for a region outside the hardcoded set', () => { + // apex host + expect(buildRegionalV1Endpoint('https:', 'cloud.appwrite.io', 'blr', true)).toBe( + 'https://blr.cloud.appwrite.io/v1' + ); + // host that already carries a known region label + expect(buildRegionalV1Endpoint('https:', 'fra.cloud.appwrite.io', 'blr', true)).toBe( + 'https://blr.cloud.appwrite.io/v1' + ); + }); + + it('does not double the subdomain when the host already carries the target region', () => { + expect(buildRegionalV1Endpoint('https:', 'blr.cloud.appwrite.io', 'blr', true)).toBe( + 'https://blr.cloud.appwrite.io/v1' + ); + }); +}); + +describe('stripLeadingRegionSubdomain', () => { + it('strips a known leading region label', () => { + expect(stripLeadingRegionSubdomain('fra.cloud.appwrite.io')).toBe('cloud.appwrite.io'); + }); + + it('collapses doubled region labels', () => { + expect(stripLeadingRegionSubdomain('fra.fra.cloud.appwrite.io')).toBe('cloud.appwrite.io'); + }); + + it('leaves a bare apex host untouched', () => { + expect(stripLeadingRegionSubdomain('cloud.appwrite.io')).toBe('cloud.appwrite.io'); + }); +}); diff --git a/src/lib/helpers/apiEndpoint.ts b/src/lib/helpers/apiEndpoint.ts index 1bcc1bf3bd..8de18c7e28 100644 --- a/src/lib/helpers/apiEndpoint.ts +++ b/src/lib/helpers/apiEndpoint.ts @@ -1,10 +1,4 @@ import { - REGION_FRA, - REGION_NYC, - REGION_SFO, - REGION_SGP, - REGION_SYD, - REGION_TOR, SUBDOMAIN_FRA, SUBDOMAIN_NYC, SUBDOMAIN_SFO, @@ -13,6 +7,12 @@ import { SUBDOMAIN_TOR } from '$lib/constants'; +/** + * The placeholder region used for region-less projects. It maps to the apex + * domain (no subdomain) and must never be turned into a subdomain. + */ +const DEFAULT_REGION = 'default'; + /** Ordered list of region DNS prefixes (e.g. `fra.`) for stripping from API hostnames. */ const REGION_SUBDOMAIN_PREFIXES: readonly string[] = [ SUBDOMAIN_FRA, @@ -26,14 +26,21 @@ const REGION_SUBDOMAIN_PREFIXES: readonly string[] = [ /** * Removes leading Appwrite Cloud region label(s) from `hostname` (e.g. `fra.`). * Strips repeatedly so a doubled prefix (`fra.fra.cloud...`) does not become - * `nyc.fra.cloud...` after prepending another region. + * `nyc.fra.cloud...` after prepending another region. An optional `extraPrefix` + * (the region about to be prepended) is stripped too, so regions outside the + * well-known set do not get doubled up. */ -export function stripLeadingRegionSubdomain(hostname: string): string { +export function stripLeadingRegionSubdomain(hostname: string, extraPrefix?: string): string { + const prefixes = + extraPrefix && !REGION_SUBDOMAIN_PREFIXES.includes(extraPrefix) + ? [extraPrefix, ...REGION_SUBDOMAIN_PREFIXES] + : REGION_SUBDOMAIN_PREFIXES; + let host = hostname; let changed = true; while (changed) { changed = false; - for (const prefix of REGION_SUBDOMAIN_PREFIXES) { + for (const prefix of prefixes) { if (host.startsWith(prefix)) { host = host.slice(prefix.length); changed = true; @@ -44,24 +51,17 @@ export function stripLeadingRegionSubdomain(hostname: string): string { return host; } -/** Region prefix (e.g. `fra.`) used before the API hostname when multi-region is enabled. */ +/** + * Region prefix (e.g. `fra.`) used before the API hostname when multi-region is + * enabled. The set of Appwrite Cloud regions is dynamic and grows over time, so + * the prefix is derived directly from the region id rather than a hardcoded + * list — otherwise projects in a newly added region resolve to no subdomain, + * hit the apex domain, and fail with CORS errors. The `default` placeholder + * (and empty/undefined values) resolve to the apex domain. + */ export function getRegionSubdomain(region?: string): string { - switch (region) { - case REGION_FRA: - return SUBDOMAIN_FRA; - case REGION_SYD: - return SUBDOMAIN_SYD; - case REGION_NYC: - return SUBDOMAIN_NYC; - case REGION_SFO: - return SUBDOMAIN_SFO; - case REGION_SGP: - return SUBDOMAIN_SGP; - case REGION_TOR: - return SUBDOMAIN_TOR; - default: - return ''; - } + if (!region || region === DEFAULT_REGION) return ''; + return `${region}.`; } /** @@ -85,6 +85,6 @@ export function buildRegionalV1Endpoint( return `${protocol}//${hostname}/v1`; } - const hostWithoutRegion = stripLeadingRegionSubdomain(hostname); + const hostWithoutRegion = stripLeadingRegionSubdomain(hostname, subdomain); return `${protocol}//${subdomain}${hostWithoutRegion}/v1`; }