/** * 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, extractSpanContextFromHeaders, type Span } from './logger.ts'; import { formatEdgeError, toError } from './errorFormatter.ts'; import { ValidationError, logValidationError } from './typeValidation.ts'; export interface EdgeFunctionConfig { name: string; requireAuth?: boolean; corsHeaders?: HeadersInit; logRequests?: boolean; logResponses?: boolean; } export interface EdgeFunctionContext { requestId: string; span: Span; userId?: string; } 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, 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: Authentication (if required) // ==================================================================== let userId: string | undefined; if (requireAuth) { addSpanEvent(span, 'authentication_start'); const authHeader = req.headers.get('Authorization'); 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' } } ); } // Extract user ID from JWT (simplified - extend as needed) try { // Note: In production, validate the JWT properly const token = authHeader.replace('Bearer ', ''); const payload = JSON.parse(atob(token.split('.')[1])); userId = payload.sub; addSpanEvent(span, 'authentication_success', { userId }); span.attributes['user.id'] = userId; } catch (error) { addSpanEvent(span, 'authentication_failed', { reason: 'invalid_token', error: formatEdgeError(error) }); endSpan(span, 'error', error); logSpan(span); return new Response( JSON.stringify({ error: 'Invalid authentication token', requestId }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } } // ==================================================================== // STEP 5: Execute handler // ==================================================================== addSpanEvent(span, 'handler_start'); const context: EdgeFunctionContext = { requestId, span, userId, }; const response = await handler(req, context); // ==================================================================== // STEP 6: Log success response // ==================================================================== addSpanEvent(span, 'handler_complete', { status: response.status, statusText: response.statusText }); if (logResponses) { edgeLogger.info('Request completed', { requestId, action: name, status: response.status, duration: span.endTime ? span.duration : Date.now() - span.startTime, }); } endSpan(span, 'ok'); logSpan(span); return response; } 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); 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' }, } ); } // 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); return new Response( JSON.stringify({ error: message, code: errorObj.code, details: errorObj.details, requestId, }), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, } ); } // 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); return new Response( JSON.stringify({ error: 'Internal server error', message: errorMessage, requestId, }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, } ); } }; } /** * 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); }