mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 19:31:14 -05:00
Add comprehensive edge-function error handling
Enhance error handling and logging across all edge functions: - Introduce a shared edgeFunctionWrapper with standardized error handling, request/response logging, tracing, and validation hooks. - Add runtime type validation utilities (ValidationError, validators, and parse/validate helpers) and integrate into edge flow. - Implement robust validation for incoming requests and known type mismatches, with detailed logs and structured responses. - Add post-RPC and post-database error logging to surface type/mismatch issues early. - Update approval/rejection entry points to leverage new validators and centralized error handling.
This commit is contained in:
341
supabase/functions/_shared/edgeFunctionWrapper.ts
Normal file
341
supabase/functions/_shared/edgeFunctionWrapper.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* 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<Response>;
|
||||
|
||||
/**
|
||||
* Wrap an edge function with comprehensive error handling
|
||||
*/
|
||||
export function wrapEdgeFunction(
|
||||
config: EdgeFunctionConfig,
|
||||
handler: EdgeFunctionHandler
|
||||
): (req: Request) => Promise<Response> {
|
||||
const {
|
||||
name,
|
||||
requireAuth = true,
|
||||
corsHeaders = {},
|
||||
logRequests = true,
|
||||
logResponses = true,
|
||||
} = config;
|
||||
|
||||
return async (req: Request): Promise<Response> => {
|
||||
// ========================================================================
|
||||
// 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<Response> {
|
||||
return wrapEdgeFunction(config, handler);
|
||||
}
|
||||
Reference in New Issue
Block a user