From f00fabeac5707af8c6cdbfd29f94fb631e9229ea Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 5 Feb 2026 12:04:20 -0600 Subject: [PATCH] Fix #63: mostrar mensaje real de error de Edge Function en UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mejorar invokeEdge para parsear el body JSON de errores HTTP de las Edge Functions y extraer un message humano (soporta { error: { message } }, { error: "..." } y { message: "..." }). - EdgeFunctionError ahora incluye status y details; se manejan también FunctionsRelayError y FunctionsFetchError con mensajes más descriptivos. - Ajustes en el front: WizardControls muestra el mensaje real del error (no el genérico "Edge Function returned a non-2xx status code"), y se corrige navegación/logging tras crear plan IA (uso de `plan` en vez de `data` y `navigate` a `/planes/{plan.id}`). - Actualización de types/API: renombrados campos en AIGeneratePlanInput para alinear nombres (descripcionEnfoqueAcademico, instruccionesAdicionalesIA). --- .gitignore | 1 + .../planes/wizard/WizardControls.tsx | 8 +- src/data/api/plans.api.ts | 4 +- src/data/supabase/invokeEdge.ts | 90 +++++++++++++++---- 4 files changed, 78 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 58b8598..3b8d608 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ count.txt .nitro .tanstack .wrangler +diff.txt \ No newline at end of file diff --git a/src/components/planes/wizard/WizardControls.tsx b/src/components/planes/wizard/WizardControls.tsx index 35f5419..24ea695 100644 --- a/src/components/planes/wizard/WizardControls.tsx +++ b/src/components/planes/wizard/WizardControls.tsx @@ -77,11 +77,11 @@ export function WizardControls({ console.log(`${new Date().toISOString()} - Enviando a generar plan IA`) - const data = await generatePlanAI.mutateAsync(aiInput as any) - console.log(`${new Date().toISOString()} - Plan IA generado`, data) + const plan = await generatePlanAI.mutateAsync(aiInput as any) + console.log(`${new Date().toISOString()} - Plan IA generado`, plan) navigate({ - to: `/planes/${data.plan.id}`, + to: `/planes/${plan.id}`, state: { showConfetti: true }, }) return @@ -122,7 +122,7 @@ export function WizardControls({ -
+
{errorMessage && ( {errorMessage} diff --git a/src/data/api/plans.api.ts b/src/data/api/plans.api.ts index 0ef5658..9b869ab 100644 --- a/src/data/api/plans.api.ts +++ b/src/data/api/plans.api.ts @@ -268,8 +268,8 @@ export type AIGeneratePlanInput = { estructuraPlanId: UUID } iaConfig: { - descripcionEnfoque: string - notasAdicionales?: string + descripcionEnfoqueAcademico: string + instruccionesAdicionalesIA?: string archivosReferencia?: Array repositoriosIds?: Array archivosAdjuntos: Array diff --git a/src/data/supabase/invokeEdge.ts b/src/data/supabase/invokeEdge.ts index 627b6eb..2aeff2e 100644 --- a/src/data/supabase/invokeEdge.ts +++ b/src/data/supabase/invokeEdge.ts @@ -1,12 +1,18 @@ -import { supabaseBrowser } from "./client"; +import { + FunctionsFetchError, + FunctionsHttpError, + FunctionsRelayError, +} from '@supabase/supabase-js' -import type { Database } from "@/types/supabase"; -import type { SupabaseClient } from "@supabase/supabase-js"; +import { supabaseBrowser } from './client' + +import type { Database } from '@/types/supabase' +import type { SupabaseClient } from '@supabase/supabase-js' export type EdgeInvokeOptions = { - method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; - headers?: Record; -}; + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' + headers?: Record +} export class EdgeFunctionError extends Error { constructor( @@ -15,8 +21,8 @@ export class EdgeFunctionError extends Error { public readonly status?: number, public readonly details?: unknown, ) { - super(message); - this.name = "EdgeFunctionError"; + super(message) + this.name = 'EdgeFunctionError' } } @@ -34,23 +40,69 @@ export async function invokeEdge( opts: EdgeInvokeOptions = {}, client?: SupabaseClient, ): Promise { - const supabase = client ?? supabaseBrowser(); + const supabase = client ?? supabaseBrowser() const { data, error } = await supabase.functions.invoke(functionName, { body, - method: opts.method ?? "POST", + method: opts.method ?? 'POST', headers: opts.headers, - }); + }) if (error) { - const anyErr = error; - throw new EdgeFunctionError( - anyErr.message ?? "Error en Edge Function", - functionName, - anyErr.status, - anyErr, - ); + // Valores por defecto (por si falla el parseo o es otro tipo de error) + let message = error.message // El genérico "returned a non-2xx status code" + let status = undefined + let details: unknown = error + + // 2. Verificamos si es un error HTTP (4xx o 5xx) que trae cuerpo JSON + if (error instanceof FunctionsHttpError) { + try { + // Obtenemos el status real (ej. 404, 400) + status = error.context.status + + // ¡LA CLAVE! Leemos el JSON que tu Edge Function envió + const errorBody = await error.context.json() + details = errorBody + + // Intentamos extraer el mensaje humano según tu estructura { error: { message: "..." } } + // o la estructura simple { error: "..." } + if (errorBody && typeof errorBody === 'object') { + // Caso 1: Estructura anidada (la que definimos hace poco: { error: { message: "..." } }) + if ( + 'error' in errorBody && + typeof errorBody.error === 'object' && + errorBody.error !== null && + 'message' in errorBody.error + ) { + message = (errorBody.error as { message: string }).message + } + // Caso 2: Estructura simple ({ error: "Mensaje de error" }) + else if ( + 'error' in errorBody && + typeof errorBody.error === 'string' + ) { + message = errorBody.error + } + // Caso 3: Propiedad message directa ({ message: "..." }) + else if ( + 'message' in errorBody && + typeof errorBody.message === 'string' + ) { + message = errorBody.message + } + } + } catch (e) { + console.warn('No se pudo parsear el error JSON de la Edge Function', e) + } + } else if (error instanceof FunctionsRelayError) { + message = `Error de Relay Supabase: ${error.message}` + } else if (error instanceof FunctionsFetchError) { + message = `Error de conexión (Fetch): ${error.message}` + } + + // 3. Lanzamos tu error personalizado con los datos reales extraídos + throw new EdgeFunctionError(message, functionName, status, details) } - return data as TOut; + return data as TOut }