feat(studio-local): functions management api - test functions (#42350)

Feature

## What is the current behavior?

Functions page on self-hosted differs from Platform

## What is the new behavior?

Adds the possibility to try/test functions in Self-Host version.

## Summary by CodeRabbit

* **Bug Fixes**
* Improved edge function URL validation so testing works reliably both
on-platform and off-platform, including proper URL handling for local
setups.

* **UI Improvements**
* Moved the Test button in the edge functions interface for more
consistent layout while preserving its behavior.

* **Tests**
  * Expanded tests to cover platform-aware URL validation scenarios.

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
This commit is contained in:
Kalleby SantosandCharis Lam authored and GitHub committed 2026-02-11 21:26:42 +00:00
1 parent 6e4aeed6d9
commit 70510acf5b
4 files changed
+76 -30

No files matched your search

@@ -299,27 +299,25 @@ const EdgeFunctionDetailsLayout = ({
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
{IS_PLATFORM && (
<>
{!!functionSlug && (
<Button
type="default"
icon={<Send />}
onClick={() => {
setIsOpen(true)
sendEvent({
action: 'edge_function_test_side_panel_opened',
groups: {
project: ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}}
>
Test
</Button>
)}
</>
{!!functionSlug && (
<Button
type="default"
icon={<Send />}
onClick={() => {
setIsOpen(true)
if (IS_PLATFORM) {
sendEvent({
action: 'edge_function_test_side_panel_opened',
groups: {
project: ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}
}}
>
Test
</Button>
)}
</div>
</PageHeaderAside>
+42 -6
View File
@@ -1,4 +1,5 @@
import { expect, describe, it } from 'vitest'
import { describe, expect, it } from 'vitest'
import { isValidEdgeFunctionURL } from './edgeFunctions'
describe('isValidEdgeFunctionURL', () => {
@@ -9,7 +10,16 @@ describe('isValidEdgeFunctionURL', () => {
'https://projectref.supabase.red/functions/v3/hello-world',
]
const invalidEdgeFunctionUrls = [
const validLocalEdgeFunctionsUrls = [
'https://projectref.notsupabase.com/functions/v1/test',
'https://notsupabase.com/functions/v1/test',
'http://localhost:54321/functions/v1/test-2',
'http://kong:8000/functions/v1/hello-world',
'https://127.0.0.1:54321/functions/v1/test-3',
'https://127.0.0.1:54321/functions/v1/test-5',
]
const invalidPlatformEdgeFunctionUrls = [
'https://notsupabase.com/functions/v1/test',
'https://projectref.notsupabase.com/functions/v1/test',
'https://localhost?https://aaaa.supabase.co/functions/v1/xxx',
@@ -17,15 +27,41 @@ describe('isValidEdgeFunctionURL', () => {
'http://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
]
it('should match valid edge function URLs', () => {
const invalidEdgeFunctionUrls = [
'https://localhost?https://aaaa.supabase.co/functions/v1/xxx',
'https://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
'http://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
]
it('should match valid edge function URLs on platform', () => {
for (const url of validEdgeFunctionUrls) {
expect(isValidEdgeFunctionURL(url), `Expected ${url} to be valid`).toBe(true)
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be valid`).toBe(true)
}
})
it('should not match invalid edge function URLs', () => {
it('should not match local URLs on platform', () => {
for (const url of validLocalEdgeFunctionsUrls) {
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid on platform`).toBe(
false
)
}
})
it('should match valid local edge function URLs off platform', () => {
for (const url of validLocalEdgeFunctionsUrls) {
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be valid`).toBe(true)
}
})
it('should not match invalid edge function URLs on platform', () => {
for (const url of invalidPlatformEdgeFunctionUrls) {
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid`).toBe(false)
}
})
it('should not match invalid edge function URLs off platform', () => {
for (const url of invalidEdgeFunctionUrls) {
expect(isValidEdgeFunctionURL(url), `Expected ${url} to be invalid`).toBe(false)
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be invalid`).toBe(false)
}
})
})
+9 -1
View File
@@ -1,12 +1,20 @@
const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL
export const isValidEdgeFunctionURL = (url: string) => {
export const isValidEdgeFunctionURL = (url: string, isPlatform: boolean) => {
if (NIMBUS_PROD_PROJECTS_URL !== undefined) {
const apexDomain = NIMBUS_PROD_PROJECTS_URL.replace('https://*.', '').replace(/\./g, '\\.')
const nimbusRegex = new RegExp('^https://[a-z]*\\.' + apexDomain + '/functions/v[0-9]{1}/.*$')
return nimbusRegex.test(url)
}
if (!isPlatform) {
const regexValidLocalEdgeFunctionURL = new RegExp(
'^https?://[^\\s/?#]+/functions/v[0-9]{1}/.*$'
)
return regexValidLocalEdgeFunctionURL.test(url)
}
const regexValidEdgeFunctionURL = new RegExp(
'^https://[a-z]*.supabase.(red|co)/functions/v[0-9]{1}/.*$'
)
+6 -2
View File
@@ -1,3 +1,4 @@
import { IS_PLATFORM } from 'common'
import { isValidEdgeFunctionURL } from 'lib/api/edgeFunctions'
import { NextApiRequest, NextApiResponse } from 'next'
@@ -20,9 +21,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
try {
const { url, method, body: requestBody, headers: customHeaders } = req.body
const { url: requestUrl, method, body: requestBody, headers: customHeaders } = req.body
const url = IS_PLATFORM
? requestUrl
: requestUrl.replace(process.env.SUPABASE_PUBLIC_URL, process.env.SUPABASE_URL)
const validEdgeFnUrl = isValidEdgeFunctionURL(url)
const validEdgeFnUrl = isValidEdgeFunctionURL(url, IS_PLATFORM)
if (!validEdgeFnUrl) {
return res.status(400).json({