From d0d41e00d6367111e219690454452df0da5eb4aa Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 20 Apr 2026 18:55:54 +0800 Subject: [PATCH] [FE-3035] fix(studio): show /rest/v1/ suffix on Data API overview URL (#45045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Data API overview page (`/integrations/data_api/overview`) was showing the project URL as `https://xxx.supabase.co`, but the documented Data API base URL is `https://xxx.supabase.co/rest/v1/`. This normalizes the URL so it matches the docs. **Changed:** - `getApiEndpoint` now appends `/rest/v1/` to the resolved endpoint (only used by the Data API overview card, so no other dashboard URLs are affected) ## To test - Visit `/dashboard/project/_/integrations/data_api/overview` and confirm the API URL field ends with `/rest/v1/` - Switch the database selector between primary, a read replica, and (if available) a load balancer — all should show a URL ending in `/rest/v1/` - With a custom domain active, the custom domain URL should also end with `/rest/v1/` Addresses [FE-3035](https://linear.app/supabase/issue/FE-3035/dashboard-data-api-page-shows-inconsistent-api-url) ## Summary by CodeRabbit ## Release Notes * **Bug Fixes** * API endpoints are now properly normalized to ensure consistent path formatting with the `/rest/v1/` suffix across all endpoint sources. * Fixed URL handling for custom domain and load balancer endpoint selection. * Enhanced replica database URL handling to ensure correct trailing slash formatting. * **Tests** * Updated test expectations and added new test cases to verify proper endpoint normalization behavior. Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../DataApi/DataApi.utils.test.ts | 31 ++++++++++++++----- .../Integrations/DataApi/DataApi.utils.ts | 15 ++++++--- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts index f39388b27dc..a94fceac858 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.test.ts @@ -13,7 +13,7 @@ const makeDatabase = ( const makeLoadBalancer = (endpoint: string): Pick => ({ endpoint }) describe('getApiEndpoint', () => { - it('returns custom domain URL when custom domain is active and primary database is selected', () => { + it('returns custom domain URL with /rest/v1/ when custom domain is active and primary database is selected', () => { expect( getApiEndpoint({ selectedDatabaseId: 'project-ref', @@ -22,10 +22,10 @@ describe('getApiEndpoint', () => { loadBalancers: undefined, selectedDatabase: makeDatabase( 'project-ref', - 'https://project-ref.supabase.co/rest/v1' + 'https://project-ref.supabase.co/rest/v1/' ) as Database, }) - ).toBe('https://api.example.com') + ).toBe('https://api.example.com/rest/v1/') }) it('returns database restUrl when custom domain is active but a replica is selected', () => { @@ -35,15 +35,30 @@ describe('getApiEndpoint', () => { projectRef: 'project-ref', resolvedEndpoint: 'https://api.example.com', loadBalancers: undefined, + selectedDatabase: makeDatabase( + 'replica-1', + 'https://replica-1.supabase.co/rest/v1/' + ) as Database, + }) + ).toBe('https://replica-1.supabase.co/rest/v1/') + }) + + it('normalizes a replica restUrl without a trailing slash', () => { + expect( + getApiEndpoint({ + selectedDatabaseId: 'replica-1', + projectRef: 'project-ref', + resolvedEndpoint: undefined, + loadBalancers: undefined, selectedDatabase: makeDatabase( 'replica-1', 'https://replica-1.supabase.co/rest/v1' ) as Database, }) - ).toBe('https://replica-1.supabase.co/rest/v1') + ).toBe('https://replica-1.supabase.co/rest/v1/') }) - it('returns load balancer endpoint when load balancer is selected', () => { + it('returns load balancer endpoint with /rest/v1/ when load balancer is selected', () => { expect( getApiEndpoint({ selectedDatabaseId: 'load-balancer', @@ -52,7 +67,7 @@ describe('getApiEndpoint', () => { loadBalancers: [makeLoadBalancer('https://lb.supabase.co') as LoadBalancer], selectedDatabase: undefined, }) - ).toBe('https://lb.supabase.co') + ).toBe('https://lb.supabase.co/rest/v1/') }) it('returns empty string when load balancer is selected but none exist', () => { @@ -76,10 +91,10 @@ describe('getApiEndpoint', () => { loadBalancers: undefined, selectedDatabase: makeDatabase( 'replica-2', - 'https://replica-2.supabase.co/rest/v1' + 'https://replica-2.supabase.co/rest/v1/' ) as Database, }) - ).toBe('https://replica-2.supabase.co/rest/v1') + ).toBe('https://replica-2.supabase.co/rest/v1/') }) }) diff --git a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts index da0fd5d6be1..060b05c619f 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts +++ b/apps/studio/components/interfaces/Integrations/DataApi/DataApi.utils.ts @@ -5,7 +5,8 @@ import { snakeToCamel } from '@/lib/helpers' /** * Resolves the API endpoint URL based on the selected database, custom domain - * status, and load balancer configuration. + * status, and load balancer configuration. The returned URL is normalized to + * end with `/rest/v1/` to match the Data API base path documented elsewhere. */ export function getApiEndpoint({ selectedDatabaseId, @@ -23,14 +24,20 @@ export function getApiEndpoint({ const loadBalancerSelected = selectedDatabaseId === 'load-balancer' if (selectedDatabaseId === projectRef && !!resolvedEndpoint) { - return resolvedEndpoint + return withDataApiPath(resolvedEndpoint) } if (loadBalancerSelected) { - return loadBalancers?.[0]?.endpoint ?? '' + return withDataApiPath(loadBalancers?.[0]?.endpoint) } - return selectedDatabase?.restUrl ?? '' + return withDataApiPath(selectedDatabase?.restUrl) +} + +function withDataApiPath(url: string | undefined): string { + if (!url) return '' + const trimmed = url.replace(/\/+$/, '') + return /\/rest\/v1$/.test(trimmed) ? `${trimmed}/` : `${trimmed}/rest/v1/` } export type EnrichedEntity = { id: string; displayName: string; camelCase: string }