Files
supabase/apps/studio/lib/validation/http-url.ts
Danny White fd17b246e1 fix(studio): tighten webhook endpoint validation (#43892)
## What kind of change does this PR introduce?

Bug fix.

## What is the current behavior?

Webhook endpoint validation is inconsistent across Studio forms. The
webhook sheet accepts incomplete hostnames like `https://webhook`, event
type validation is not surfaced clearly, and HTTP endpoint validation
differs between webhooks, log drains, cron jobs, and database hooks.

## What is the new behavior?

- Tightens webhook endpoint URL validation and rejects incomplete
hostnames while still allowing localhost and IP-based endpoints.
- Surfaces the Event types validation through the standard form error
styling and highlights the existing accordion item border when invalid.
- **Extracts a shared HTTP endpoint URL validator and reuses it in
webhooks, log drains, cron jobs, and database hooks.**
- Adds focused regression tests for the webhook sheet and the
shared/consumer validation paths.

| After |
| --- |
| <img width="1728" height="997" alt="Webhooks Settings AWS Healthy
Toolshed Supabase-CB0D999C-D0BF-47AA-A10F-342A2E328DF9"
src="https://github.com/user-attachments/assets/bcbe4876-f9a7-497a-b288-460087a65546"
/> |

## To test

Form behaviour (in particular URL validation) on:

- Webhook endpoint
- Log drains
- Cron jobs
- Database hooks
2026-03-19 11:43:02 +11:00

60 lines
1.4 KiB
TypeScript

import { z } from 'zod'
const HTTP_URL_PROTOCOL_REGEX = /^https?:\/\//
const IPV4_SEGMENT = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)'
const IPV4_REGEX = new RegExp(`^(?:${IPV4_SEGMENT}\\.){3}${IPV4_SEGMENT}$`)
const BRACKETED_IPV6_REGEX = /^\[[0-9a-f:.]+\]$/i
export const hasHttpUrlProtocol = (value: string) => HTTP_URL_PROTOCOL_REGEX.test(value)
export const isValidHttpEndpointUrl = (value: string) => {
try {
const url = new URL(value)
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
const { hostname } = url
return (
hostname === 'localhost' ||
hostname.includes('.') ||
IPV4_REGEX.test(hostname) ||
BRACKETED_IPV6_REGEX.test(hostname)
)
} catch {
return false
}
}
type HttpEndpointUrlSchemaOptions = {
requiredMessage: string
invalidMessage: string
prefixMessage: string
}
export const httpEndpointUrlSchema = ({
requiredMessage,
invalidMessage,
prefixMessage,
}: HttpEndpointUrlSchemaOptions) =>
z
.string()
.trim()
.min(1, requiredMessage)
.superRefine((value, ctx) => {
if (!value) return
if (!hasHttpUrlProtocol(value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: prefixMessage,
})
return
}
if (!isValidHttpEndpointUrl(value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: invalidMessage,
})
}
})