Files
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

39 lines
1.4 KiB
TypeScript

import { z } from 'zod'
import { httpEndpointUrlSchema } from '@/lib/validation/http-url'
const httpRequestSchema = z.object({
function_type: z.literal('http_request'),
http_url: httpEndpointUrlSchema({
requiredMessage: 'Please provide a URL',
invalidMessage: 'Please provide a valid URL',
prefixMessage: 'Please prefix your URL with http:// or https://',
}),
})
const supabaseFunctionSchema = z.object({
function_type: z.literal('supabase_function'),
http_url: z
.string()
.min(1, 'Please select an edge function')
.refine((val) => !val.includes('undefined'), 'No edge functions available for selection'),
})
export const FormSchema = z
.object({
name: z.string().min(1, 'Please provide a name for your webhook'),
table_id: z.string().min(1, 'Please select a table'),
http_method: z.enum(['GET', 'POST']),
timeout_ms: z.coerce
.number()
.int()
.gte(1000, 'Timeout should be at least 1000ms')
.lte(10000, 'Timeout should not exceed 10,000ms'),
events: z.array(z.string()).min(1, 'Please select at least one event'),
httpHeaders: z.array(z.object({ id: z.string(), name: z.string(), value: z.string() })),
httpParameters: z.array(z.object({ id: z.string(), name: z.string(), value: z.string() })),
})
.and(z.discriminatedUnion('function_type', [httpRequestSchema, supabaseFunctionSchema]))
export type WebhookFormValues = z.infer<typeof FormSchema>