diff --git a/supabase/functions/_shared/errorFormatter.ts b/supabase/functions/_shared/errorFormatter.ts new file mode 100644 index 00000000..2fa90b43 --- /dev/null +++ b/supabase/functions/_shared/errorFormatter.ts @@ -0,0 +1,94 @@ +/** + * Error Formatting Utility for Edge Functions + * + * Provides robust error message extraction from various error types: + * - Standard Error objects + * - Supabase PostgresError objects (plain objects with message/details/code/hint) + * - Raw objects and primitives + * + * Eliminates "[object Object]" errors by properly extracting error details. + */ + +/** + * Format error objects for logging + * Handles Error objects, Supabase errors (plain objects), and primitives + * + * @param error - Any error value + * @returns Formatted, human-readable error message string + */ +export function formatEdgeError(error: unknown): string { + // Standard Error objects + if (error instanceof Error) { + return error.message; + } + + // Object-like errors (Supabase PostgresError, etc.) + if (typeof error === 'object' && error !== null) { + const err = error as any; + + // Try common error message properties + if (err.message && typeof err.message === 'string') { + // Include additional Supabase error details if present + const parts: string[] = [err.message]; + + if (err.details && typeof err.details === 'string') { + parts.push(`Details: ${err.details}`); + } + + if (err.hint && typeof err.hint === 'string') { + parts.push(`Hint: ${err.hint}`); + } + + if (err.code && typeof err.code === 'string') { + parts.push(`Code: ${err.code}`); + } + + return parts.join(' | '); + } + + // Some errors nest the actual error in an 'error' property + if (err.error) { + return formatEdgeError(err.error); + } + + // Some APIs use 'msg' instead of 'message' + if (err.msg && typeof err.msg === 'string') { + return err.msg; + } + + // Last resort: stringify the entire object + try { + const stringified = JSON.stringify(error, null, 2); + return stringified.length > 500 + ? stringified.substring(0, 500) + '... (truncated)' + : stringified; + } catch { + // JSON.stringify can fail on circular references + return 'Unknown error (could not stringify)'; + } + } + + // Primitive values (strings, numbers, etc.) + return String(error); +} + +/** + * Convert any error to a proper Error instance + * Use this before throwing to ensure proper stack traces + * + * @param error - Any error value + * @returns Error instance with formatted message + */ +export function toError(error: unknown): Error { + if (error instanceof Error) { + return error; + } + + const message = formatEdgeError(error); + const newError = new Error(message); + + // Preserve original error as property for debugging + (newError as any).originalError = error; + + return newError; +} diff --git a/supabase/functions/_shared/logger.ts b/supabase/functions/_shared/logger.ts index 3b655c81..1a6219f5 100644 --- a/supabase/functions/_shared/logger.ts +++ b/supabase/functions/_shared/logger.ts @@ -3,6 +3,8 @@ * Prevents sensitive data exposure and provides consistent log format */ +import { formatEdgeError } from './errorFormatter.ts'; + type LogLevel = 'info' | 'warn' | 'error' | 'debug'; interface LogContext { @@ -96,16 +98,17 @@ export function startSpan( /** * End a span with final status */ -export function endSpan(span: Span, status?: 'ok' | 'error', error?: Error): Span { +export function endSpan(span: Span, status?: 'ok' | 'error', error?: unknown): Span { span.endTime = Date.now(); span.duration = span.endTime - span.startTime; span.status = status || 'ok'; if (error) { + const err = error instanceof Error ? error : new Error(formatEdgeError(error)); span.error = { - type: error.name, - message: error.message, - stack: error.stack, + type: err.name, + message: err.message, + stack: err.stack, }; } diff --git a/supabase/functions/admin-delete-user/index.ts b/supabase/functions/admin-delete-user/index.ts index bec05753..fe816f89 100644 --- a/supabase/functions/admin-delete-user/index.ts +++ b/supabase/functions/admin-delete-user/index.ts @@ -1,5 +1,6 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -348,7 +349,7 @@ Deno.serve(async (req) => { edgeLogger.warn('Error deleting avatar from Cloudflare', { requestId: tracking.requestId, targetUserId, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), action: 'admin_delete_user' }); } @@ -417,7 +418,7 @@ Deno.serve(async (req) => { edgeLogger.warn('Error removing Novu subscriber', { requestId: tracking.requestId, targetUserId, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), action: 'admin_delete_user' }); } @@ -521,7 +522,7 @@ Deno.serve(async (req) => { edgeLogger.warn('Error sending deletion notification email', { requestId: tracking.requestId, targetUserId, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), action: 'admin_delete_user' }); } @@ -546,7 +547,7 @@ Deno.serve(async (req) => { edgeLogger.error('Unexpected error in admin delete user', { requestId: tracking.requestId, duration, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), action: 'admin_delete_user' }); diff --git a/supabase/functions/cancel-account-deletion/index.ts b/supabase/functions/cancel-account-deletion/index.ts index b8739c75..2b33c0f4 100644 --- a/supabase/functions/cancel-account-deletion/index.ts +++ b/supabase/functions/cancel-account-deletion/index.ts @@ -1,6 +1,7 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -137,7 +138,7 @@ serve(async (req) => { ); } catch (error) { const duration = endRequest(tracking); - edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: error instanceof Error ? error.message : String(error), requestId: tracking.requestId, duration }); + edgeLogger.error('Error cancelling deletion', { action: 'cancel_deletion_error', error: formatEdgeError(error), requestId: tracking.requestId, duration }); return new Response( JSON.stringify({ error: error.message, requestId: tracking.requestId }), { diff --git a/supabase/functions/cancel-email-change/index.ts b/supabase/functions/cancel-email-change/index.ts index 4b3a14c4..518580dc 100644 --- a/supabase/functions/cancel-email-change/index.ts +++ b/supabase/functions/cancel-email-change/index.ts @@ -1,5 +1,6 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -149,7 +150,7 @@ Deno.serve(async (req) => { action: 'cancel_email_change', requestId: tracking.requestId, duration, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); return new Response( JSON.stringify({ diff --git a/supabase/functions/cleanup-old-versions/index.ts b/supabase/functions/cleanup-old-versions/index.ts index 8f7e87b6..35a6b224 100644 --- a/supabase/functions/cleanup-old-versions/index.ts +++ b/supabase/functions/cleanup-old-versions/index.ts @@ -1,5 +1,6 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { edgeLogger } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -183,7 +184,7 @@ Deno.serve(async (req) => { } ); } catch (error) { - edgeLogger.error('Cleanup job failed', { error: error instanceof Error ? error.message : String(error) }); + edgeLogger.error('Cleanup job failed', { error: formatEdgeError(error) }); return new Response( JSON.stringify({ diff --git a/supabase/functions/create-novu-subscriber/index.ts b/supabase/functions/create-novu-subscriber/index.ts index f20f80bb..6ae4332a 100644 --- a/supabase/functions/create-novu-subscriber/index.ts +++ b/supabase/functions/create-novu-subscriber/index.ts @@ -1,6 +1,7 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { edgeLogger } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -218,7 +219,7 @@ serve(async (req) => { } catch (topicError: unknown) { // Non-blocking - log error but don't fail the request edgeLogger.error('Failed to add subscriber to users topic', { - error: topicError instanceof Error ? topicError.message : String(topicError), + error: formatEdgeError(topicError), subscriberId, requestId: tracking.requestId }); @@ -238,7 +239,7 @@ serve(async (req) => { } catch (error: unknown) { const duration = endRequest(tracking); edgeLogger.error('Error creating Novu subscriber', { - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), requestId: tracking.requestId, duration }); diff --git a/supabase/functions/detect-location/index.ts b/supabase/functions/detect-location/index.ts index e69a5bdb..293fbc21 100644 --- a/supabase/functions/detect-location/index.ts +++ b/supabase/functions/detect-location/index.ts @@ -1,5 +1,6 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { formatEdgeError } from "../_shared/errorFormatter.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -45,7 +46,7 @@ function cleanupExpiredEntries() { // CRITICAL: Increment failure counter and log detailed error information cleanupFailureCount++; - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = formatEdgeError(error); edgeLogger.error('Cleanup error', { attempt: cleanupFailureCount, @@ -284,7 +285,7 @@ serve(async (req) => { } catch (error: unknown) { // Enhanced error logging for better visibility and debugging - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = formatEdgeError(error); edgeLogger.error('Location detection error', { error: errorMessage, diff --git a/supabase/functions/export-user-data/index.ts b/supabase/functions/export-user-data/index.ts index 0c230142..6a32dc58 100644 --- a/supabase/functions/export-user-data/index.ts +++ b/supabase/functions/export-user-data/index.ts @@ -2,6 +2,7 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { sanitizeError } from '../_shared/errorSanitizer.ts'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -348,7 +349,7 @@ serve(async (req) => { action: 'export_error', requestId: tracking.requestId, duration, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); const sanitized = sanitizeError(error, 'export-user-data'); return new Response( diff --git a/supabase/functions/mfa-unenroll/index.ts b/supabase/functions/mfa-unenroll/index.ts index 73e75480..c31d272b 100644 --- a/supabase/functions/mfa-unenroll/index.ts +++ b/supabase/functions/mfa-unenroll/index.ts @@ -1,5 +1,6 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -186,7 +187,7 @@ Deno.serve(async (req) => { action: 'mfa_unenroll_error', requestId: tracking.requestId, duration, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); return new Response( JSON.stringify({ diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 7e2755d2..2fed9865 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -13,6 +13,7 @@ import { extractSpanContextFromHeaders, type Span } from '../_shared/logger.ts'; +import { formatEdgeError, toError } from '../_shared/errorFormatter.ts'; const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || 'https://api.thrillwiki.com'; const SUPABASE_ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY')!; @@ -277,7 +278,7 @@ const handler = async (req: Request) => { } if (idempotencyError) { - throw idempotencyError; + throw toError(idempotencyError); } } @@ -401,7 +402,7 @@ const handler = async (req: Request) => { requestId, idempotencyKey, status: 'failed', - error: updateError instanceof Error ? updateError.message : String(updateError), + error: formatEdgeError(updateError), action: 'process_approval' }); // Non-blocking - continue with error response even if idempotency update fails @@ -461,7 +462,7 @@ const handler = async (req: Request) => { requestId, idempotencyKey, status: 'completed', - error: updateError instanceof Error ? updateError.message : String(updateError), + error: formatEdgeError(updateError), action: 'process_approval' }); // Non-blocking - transaction succeeded, so continue with success response @@ -483,13 +484,13 @@ const handler = async (req: Request) => { ); } catch (error) { - endSpan(rootSpan, 'error', error instanceof Error ? error : new Error(String(error))); + endSpan(rootSpan, 'error', error instanceof Error ? error : toError(error)); logSpan(rootSpan); edgeLogger.error('Unexpected error', { requestId, duration: rootSpan.duration, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), stack: error instanceof Error ? error.stack : undefined, action: 'process_approval' }); diff --git a/supabase/functions/process-selective-rejection/index.ts b/supabase/functions/process-selective-rejection/index.ts index 4c4d3e36..133cf831 100644 --- a/supabase/functions/process-selective-rejection/index.ts +++ b/supabase/functions/process-selective-rejection/index.ts @@ -13,6 +13,7 @@ import { extractSpanContextFromHeaders, type Span } from '../_shared/logger.ts'; +import { formatEdgeError, toError } from '../_shared/errorFormatter.ts'; const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || 'https://api.thrillwiki.com'; const SUPABASE_ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY')!; @@ -280,7 +281,7 @@ const handler = async (req: Request) => { } if (idempotencyError) { - throw idempotencyError; + throw toError(idempotencyError); } } @@ -404,7 +405,7 @@ const handler = async (req: Request) => { requestId, idempotencyKey, status: 'failed', - error: updateError instanceof Error ? updateError.message : String(updateError), + error: formatEdgeError(updateError), action: 'process_rejection' }); // Non-blocking - continue with error response even if idempotency update fails @@ -464,7 +465,7 @@ const handler = async (req: Request) => { requestId, idempotencyKey, status: 'completed', - error: updateError instanceof Error ? updateError.message : String(updateError), + error: formatEdgeError(updateError), action: 'process_rejection' }); // Non-blocking - transaction succeeded, so continue with success response @@ -486,13 +487,13 @@ const handler = async (req: Request) => { ); } catch (error) { - endSpan(rootSpan, 'error', error instanceof Error ? error : new Error(String(error))); + endSpan(rootSpan, 'error', error instanceof Error ? error : toError(error)); logSpan(rootSpan); edgeLogger.error('Unexpected error', { requestId, duration: rootSpan.duration, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), stack: error instanceof Error ? error.stack : undefined, action: 'process_rejection' }); diff --git a/supabase/functions/receive-inbound-email/index.ts b/supabase/functions/receive-inbound-email/index.ts index fb5a6696..eec30ca8 100644 --- a/supabase/functions/receive-inbound-email/index.ts +++ b/supabase/functions/receive-inbound-email/index.ts @@ -2,6 +2,7 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; +import { formatEdgeError } from "../_shared/errorFormatter.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -261,7 +262,7 @@ const handler = async (req: Request): Promise => { } catch (error) { edgeLogger.error('Unexpected error in receive-inbound-email', { requestId: tracking.requestId, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); return createErrorResponse(error, 500, corsHeaders); } diff --git a/supabase/functions/scheduled-maintenance/index.ts b/supabase/functions/scheduled-maintenance/index.ts index 8559e356..7f12379d 100644 --- a/supabase/functions/scheduled-maintenance/index.ts +++ b/supabase/functions/scheduled-maintenance/index.ts @@ -1,6 +1,7 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { edgeLogger } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -55,7 +56,7 @@ serve(async (req: Request) => { } catch (error) { edgeLogger.error('Maintenance exception', { requestId, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); return new Response( diff --git a/supabase/functions/send-admin-email-reply/index.ts b/supabase/functions/send-admin-email-reply/index.ts index b120422a..02aedee0 100644 --- a/supabase/functions/send-admin-email-reply/index.ts +++ b/supabase/functions/send-admin-email-reply/index.ts @@ -2,6 +2,7 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; +import { formatEdgeError } from "../_shared/errorFormatter.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -237,7 +238,7 @@ const handler = async (req: Request): Promise => { } catch (error) { edgeLogger.error('Unexpected error in send-admin-email-reply', { requestId: tracking.requestId, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); return createErrorResponse(error, 500, corsHeaders); } diff --git a/supabase/functions/send-contact-message/index.ts b/supabase/functions/send-contact-message/index.ts index bab3d3a2..c342824e 100644 --- a/supabase/functions/send-contact-message/index.ts +++ b/supabase/functions/send-contact-message/index.ts @@ -2,6 +2,7 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { edgeLogger } from "../_shared/logger.ts"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; +import { formatEdgeError } from "../_shared/errorFormatter.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -335,7 +336,7 @@ The ThrillWiki Team`, edgeLogger.error('Contact submission failed', { requestId, duration, - error: error instanceof Error ? error.message : String(error) + error: formatEdgeError(error) }); return createErrorResponse(error, 500, corsHeaders); } diff --git a/supabase/functions/sitemap/index.ts b/supabase/functions/sitemap/index.ts index 04f95410..68bbb6f4 100644 --- a/supabase/functions/sitemap/index.ts +++ b/supabase/functions/sitemap/index.ts @@ -1,5 +1,6 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { edgeLogger } from '../_shared/logger.ts'; +import { formatEdgeError } from '../_shared/errorFormatter.ts'; const BASE_URL = 'https://dev.thrillwiki.com'; @@ -347,7 +348,7 @@ Deno.serve(async (req) => { edgeLogger.error('Sitemap generation failed', { requestId, - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), duration, }); diff --git a/supabase/functions/upload-image/index.ts b/supabase/functions/upload-image/index.ts index 0f188cb3..313fe5c0 100644 --- a/supabase/functions/upload-image/index.ts +++ b/supabase/functions/upload-image/index.ts @@ -2,6 +2,7 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts" import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts' import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts' +import { formatEdgeError } from '../_shared/errorFormatter.ts' // Environment-aware CORS configuration const getAllowedOrigin = (requestOrigin: string | null): string | null => { @@ -94,7 +95,7 @@ async function reportBanEvasionToAlerts( } catch (error) { // Non-blocking - log but don't fail the response edgeLogger.warn('Failed to report ban evasion', { - error: error instanceof Error ? error.message : String(error), + error: formatEdgeError(error), requestId }); } @@ -227,8 +228,8 @@ serve(withRateLimit(async (req) => { try { requestBody = await req.json(); } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - edgeLogger.error('Invalid JSON in delete request', { + const errorMessage = formatEdgeError(error); + edgeLogger.error('Invalid JSON in delete request', { error: errorMessage, requestId: tracking.requestId }); diff --git a/supabase/functions/validate-email-backend/index.ts b/supabase/functions/validate-email-backend/index.ts index 7cca29dc..5bab24aa 100644 --- a/supabase/functions/validate-email-backend/index.ts +++ b/supabase/functions/validate-email-backend/index.ts @@ -1,6 +1,7 @@ import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3'; import { edgeLogger } from "../_shared/logger.ts"; +import { formatEdgeError } from "../_shared/errorFormatter.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -96,7 +97,7 @@ serve(async (req) => { ); } catch (error) { const duration = endRequest(tracking); - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = formatEdgeError(error); edgeLogger.error('Email validation error', { error: errorMessage, requestId: tracking.requestId, diff --git a/supabase/functions/validate-email/index.ts b/supabase/functions/validate-email/index.ts index f20ab46f..07af9146 100644 --- a/supabase/functions/validate-email/index.ts +++ b/supabase/functions/validate-email/index.ts @@ -1,5 +1,6 @@ import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { startRequest, endRequest, edgeLogger } from "../_shared/logger.ts"; +import { formatEdgeError } from "../_shared/errorFormatter.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -180,7 +181,7 @@ const handler = async (req: Request): Promise => { ); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = formatEdgeError(error); edgeLogger.error('Error in validate-email function', { error: errorMessage, requestId: tracking.requestId