/** * Edge Function Wrapper with Comprehensive Error Handling * * Provides standardized: * - Request/response logging * - Error handling and formatting * - Distributed tracing * - Type validation * - Performance monitoring */ import { edgeLogger, startSpan, endSpan, addSpanEvent, logSpan, logSpanToDatabase, extractSpanContextFromHeaders, type Span } from './logger.ts'; import { formatEdgeError, toError } from './errorFormatter.ts'; import { ValidationError, logValidationError } from './typeValidation.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; export interface EdgeFunctionConfig { name: string; requireAuth?: boolean; requiredRoles?: string[]; useServiceRole?: boolean; corsHeaders?: HeadersInit; logRequests?: boolean; logResponses?: boolean; } export interface EdgeFunctionContext { requestId: string; span: Span; userId?: string; user?: any; supabase: any; } export type EdgeFunctionHandler = ( req: Request, context: EdgeFunctionContext ) => Promise; /** * Wrap an edge function with comprehensive error handling */ export function wrapEdgeFunction( config: EdgeFunctionConfig, handler: EdgeFunctionHandler ): (req: Request) => Promise { const { name, requireAuth = true, requiredRoles = [], useServiceRole = false, corsHeaders = {}, logRequests = true, logResponses = true, } = config; return async (req: Request): Promise => { // ======================================================================== // STEP 1: Handle CORS preflight // ======================================================================== if (req.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } // ======================================================================== // STEP 2: Initialize tracing // ======================================================================== const parentSpanContext = extractSpanContextFromHeaders(req.headers); const span = startSpan( name, 'SERVER', parentSpanContext, { 'http.method': req.method, 'http.url': req.url, 'function.name': name, } ); const requestId = span.spanId; // ======================================================================== // STEP 3: Log incoming request // ======================================================================== if (logRequests) { edgeLogger.info('Request received', { requestId, action: name, method: req.method, url: req.url, hasAuth: req.headers.has('Authorization'), contentType: req.headers.get('Content-Type'), userAgent: req.headers.get('User-Agent'), }); } try { // ==================================================================== // STEP 4: Create Supabase client // ==================================================================== const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const authHeader = req.headers.get('Authorization'); let supabase; if (useServiceRole) { // Use service role key for backend operations const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; supabase = createClient(supabaseUrl, serviceRoleKey); addSpanEvent(span, 'supabase_client_created', { type: 'service_role' }); } else if (authHeader) { // Use anon key with user's auth header const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!; supabase = createClient(supabaseUrl, anonKey, { global: { headers: { Authorization: authHeader } } }); addSpanEvent(span, 'supabase_client_created', { type: 'authenticated' }); } else { // Use anon key without auth const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!; supabase = createClient(supabaseUrl, anonKey); addSpanEvent(span, 'supabase_client_created', { type: 'anonymous' }); } // ==================================================================== // STEP 5: Authentication (if required) // ==================================================================== let userId: string | undefined; let user: any = undefined; if (requireAuth) { addSpanEvent(span, 'authentication_start'); if (!authHeader) { addSpanEvent(span, 'authentication_failed', { reason: 'missing_header' }); endSpan(span, 'error'); logSpan(span); return new Response( JSON.stringify({ error: 'Missing Authorization header', requestId }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Get user from Supabase const { data: { user: authUser }, error: userError } = await supabase.auth.getUser(); if (userError || !authUser) { addSpanEvent(span, 'authentication_failed', { reason: 'invalid_token', error: formatEdgeError(userError) }); endSpan(span, 'error', userError); logSpan(span); return new Response( JSON.stringify({ error: 'Invalid authentication token', requestId }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } user = authUser; userId = authUser.id; addSpanEvent(span, 'authentication_success', { userId }); span.attributes['user.id'] = userId; // ==================================================================== // STEP 6: Role verification (if required) // ==================================================================== if (requiredRoles.length > 0) { addSpanEvent(span, 'role_check_start', { requiredRoles }); let hasRequiredRole = false; for (const role of requiredRoles) { const { data: hasRole } = await supabase .rpc('has_role', { _user_id: userId, _role: role }); if (hasRole) { hasRequiredRole = true; addSpanEvent(span, 'role_check_success', { role }); break; } } if (!hasRequiredRole) { addSpanEvent(span, 'role_check_failed', { userId, requiredRoles }); endSpan(span, 'error'); logSpan(span); return new Response( JSON.stringify({ error: 'Insufficient permissions', requestId }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } } } // ==================================================================== // STEP 7: Execute handler // ==================================================================== addSpanEvent(span, 'handler_start'); const context: EdgeFunctionContext = { requestId, span, userId, user, supabase, }; const response = await handler(req, context); // ==================================================================== // STEP 6: Log success response // ==================================================================== addSpanEvent(span, 'handler_complete', { status: response.status, statusText: response.statusText }); const duration = span.endTime ? span.duration : Date.now() - span.startTime; if (logResponses) { edgeLogger.info('Request completed', { requestId, action: name, status: response.status, duration, }); } endSpan(span, 'ok'); logSpan(span); logSpanToDatabase(span, requestId); // Clone response to add tracking headers const responseBody = await response.text(); const enhancedResponse = new Response(responseBody, { status: response.status, statusText: response.statusText, headers: { ...Object.fromEntries(response.headers.entries()), 'X-Request-Id': requestId, 'X-Span-Id': span.spanId, 'X-Trace-Id': span.traceId, 'X-Duration-Ms': duration.toString(), }, }); return enhancedResponse; } catch (error) { // ==================================================================== // STEP 7: Handle errors // ==================================================================== // Validation errors (client error) if (error instanceof ValidationError) { addSpanEvent(span, 'validation_error', { field: error.field, expected: error.expected, received: error.received, }); logValidationError(error, requestId, name); endSpan(span, 'error', error); logSpan(span); logSpanToDatabase(span, requestId); const duration = span.endTime ? span.duration : Date.now() - span.startTime; return new Response( JSON.stringify({ error: error.message, field: error.field, expected: error.expected, received: error.received, requestId, }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-Id': requestId, 'X-Span-Id': span.spanId, 'X-Trace-Id': span.traceId, 'X-Duration-Ms': duration.toString(), }, } ); } // Database errors (check for specific codes) const errorObj = error as any; if (errorObj?.code) { addSpanEvent(span, 'database_error', { code: errorObj.code, message: errorObj.message, }); // Handle specific error codes let status = 500; let message = formatEdgeError(error); if (errorObj.code === '23505') { // Unique constraint violation status = 409; message = 'A record with this information already exists'; } else if (errorObj.code === '23503') { // Foreign key violation status = 400; message = 'Referenced record does not exist'; } else if (errorObj.code === '23514') { // Check constraint violation status = 400; message = 'Data violates database constraints'; } else if (errorObj.code === 'P0001') { // Raised exception status = 400; message = errorObj.message || 'Database validation failed'; } else if (errorObj.code === '42501') { // Insufficient privilege status = 403; message = 'Permission denied'; } edgeLogger.error('Database error', { requestId, action: name, errorCode: errorObj.code, errorMessage: errorObj.message, errorDetails: errorObj.details, errorHint: errorObj.hint, }); endSpan(span, 'error', error); logSpan(span); logSpanToDatabase(span, requestId); const duration = span.endTime ? span.duration : Date.now() - span.startTime; return new Response( JSON.stringify({ error: message, code: errorObj.code, details: errorObj.details, requestId, }), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-Id': requestId, 'X-Span-Id': span.spanId, 'X-Trace-Id': span.traceId, 'X-Duration-Ms': duration.toString(), }, } ); } // Generic errors const errorMessage = formatEdgeError(error); addSpanEvent(span, 'unhandled_error', { error: errorMessage, errorType: error instanceof Error ? error.name : typeof error, }); edgeLogger.error('Unhandled error', { requestId, action: name, error: errorMessage, errorType: error instanceof Error ? error.name : typeof error, stack: error instanceof Error ? error.stack : undefined, }); endSpan(span, 'error', error); logSpan(span); logSpanToDatabase(span, requestId); const duration = span.endTime ? span.duration : Date.now() - span.startTime; return new Response( JSON.stringify({ error: 'Internal server error', message: errorMessage, requestId, }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-Id': requestId, 'X-Span-Id': span.spanId, 'X-Trace-Id': span.traceId, 'X-Duration-Ms': duration.toString(), }, } ); } }; } /** * Create a simple edge function with standard error handling * * Example usage: * ```ts * serve(createEdgeFunction({ * name: 'my-function', * requireAuth: true, * corsHeaders: myCorsHeaders, * }, async (req, { requestId, span, userId }) => { * // Your handler logic here * return new Response(JSON.stringify({ success: true }), { * status: 200, * headers: { 'Content-Type': 'application/json' } * }); * })); * ``` */ export function createEdgeFunction( config: EdgeFunctionConfig, handler: EdgeFunctionHandler ): (req: Request) => Promise { return wrapEdgeFunction(config, handler); }