Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/lib/helpers/apiEndpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
REGION_FRA,
REGION_NYC,
REGION_SFO,
REGION_SGP,
REGION_SYD,
REGION_TOR,
SUBDOMAIN_FRA,
SUBDOMAIN_NYC,
SUBDOMAIN_SFO,
SUBDOMAIN_SGP,
SUBDOMAIN_SYD,
SUBDOMAIN_TOR
} from '$lib/constants';

/** Ordered list of region DNS prefixes (e.g. `fra.`) for stripping from API hostnames. */
const REGION_SUBDOMAIN_PREFIXES: readonly string[] = [
SUBDOMAIN_FRA,
SUBDOMAIN_NYC,
SUBDOMAIN_SYD,
SUBDOMAIN_SFO,
SUBDOMAIN_SGP,
SUBDOMAIN_TOR
];

/**
* 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.
*/
export function stripLeadingRegionSubdomain(hostname: string): string {
let host = hostname;
let changed = true;
while (changed) {
changed = false;
for (const prefix of REGION_SUBDOMAIN_PREFIXES) {
if (host.startsWith(prefix)) {
host = host.slice(prefix.length);
changed = true;
break;
}
}
}
return host;
}

/** Region prefix (e.g. `fra.`) used before the API hostname when multi-region is enabled. */
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 '';
}
}

/**
* Builds the `/v1` API base URL (protocol + host + `/v1`).
* When `isMultiRegion` is true, strips any known region prefix from the host, then prepends the requested region.
*/
export function buildRegionalV1Endpoint(
protocol: string,
hostname: string,
region: string | undefined,
isMultiRegion: boolean
): string {
if (!isMultiRegion) {
return `${protocol}//${hostname}/v1`;
}

const hostWithoutRegion = stripLeadingRegionSubdomain(hostname);
const subdomain = getRegionSubdomain(region);

return `${protocol}//${subdomain}${hostWithoutRegion}/v1`;
Comment on lines +77 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Region stripped when called without a region on multi-region deployments

When buildRegionalV1Endpoint is called with isMultiRegion = true but region = undefined (which happens for the initial const endpoint = getApiEndpoint() at sdk.ts:67), the function will:

  1. Strip any existing region prefix from hostname via stripLeadingRegionSubdomain
  2. Prepend getRegionSubdomain(undefined)''

This produces a hostname without any region prefix.

The original code avoided this problem because it only mutated the hostname when subdomain was non-empty (if (subdomain && hostname.startsWith(subdomain)) ...). With getSubdomain(undefined) returning '', the original hostname passed through unchanged.

Affected scenario: Any self-hosted instance with PUBLIC_APPWRITE_MULTI_REGION=true where APPWRITE_ENDPOINT (or the browser URL) contains a region prefix (e.g. fra.example.com). Calling getApiEndpoint() with the new code would strip fra. and route the console clients to example.com/v1 instead of the intended fra.example.com/v1.

A minimal guard fixes this:

if (!isMultiRegion) {
    return `${protocol}//${hostname}/v1`;
}

const subdomain = getRegionSubdomain(region);
if (!subdomain) {
    // No specific region requested — return the hostname as-is without stripping.
    return `${protocol}//${hostname}/v1`;
}

const hostWithoutRegion = stripLeadingRegionSubdomain(hostname);
return `${protocol}//${subdomain}${hostWithoutRegion}/v1`;

}
Comment on lines +1 to +85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No unit tests for the new helper

The project convention (documented in AGENTS.md and reflected by every other helper) is to co-locate a *.test.ts file alongside each helper module. For example: array.test.ts, date.test.ts, string.test.ts, etc. live right next to their corresponding helpers.

apiEndpoint.ts introduces meaningful URL-manipulation logic (stripLeadingRegionSubdomain, getRegionSubdomain, buildRegionalV1Endpoint) but ships with no test file, making it harder to catch regressions.

Consider adding src/lib/helpers/apiEndpoint.test.ts with cases such as:

  • Multi-region disabled → hostname unchanged
  • Switching region when hostname already has a different prefix (fra.cloud.appwrite.io → region nycnyc.cloud.appwrite.io/v1)
  • Doubling the same prefix (fra.fra.cloud.appwrite.iofra.cloud.appwrite.io/v1)
  • No region requested (undefined) on a multi-region URL → hostname unchanged

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

44 changes: 2 additions & 42 deletions src/lib/stores/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,59 +28,19 @@ import {
Realtime,
Organizations
} from '@appwrite.io/console';
import { buildRegionalV1Endpoint } from '$lib/helpers/apiEndpoint';
import { Sources } from '$lib/sdk/sources';
import {
REGION_FRA,
REGION_NYC,
REGION_SYD,
REGION_SFO,
REGION_SGP,
REGION_TOR,
SUBDOMAIN_FRA,
SUBDOMAIN_NYC,
SUBDOMAIN_SFO,
SUBDOMAIN_SYD,
SUBDOMAIN_SGP,
SUBDOMAIN_TOR
} from '$lib/constants';
import { building } from '$app/environment';

export function getApiEndpoint(region?: string): string {
if (building) return '';
const url = new URL(
VARS.APPWRITE_ENDPOINT ? VARS.APPWRITE_ENDPOINT : globalThis?.location?.toString()
);
const protocol = url.protocol;
const hostname = url.host; // "hostname:port" (or just "hostname" if no port)

// If instance supports multi-region, add the region subdomain.
let subdomain = isMultiRegionSupported(url) ? getSubdomain(region) : '';
if (subdomain && hostname.startsWith(subdomain)) {
subdomain = '';
}

return `${protocol}//${subdomain}${hostname}/v1`;
return buildRegionalV1Endpoint(url.protocol, url.host, region, isMultiRegionSupported(url));
}

const getSubdomain = (region?: 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 '';
}
};

function createConsoleSdk(client: Client) {
return {
client,
Expand Down
Loading