Fix #63: mostrar mensaje real de error de Edge Function en UI

- 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).
This commit was merged in pull request #70.
This commit is contained in:
2026-02-05 12:04:20 -06:00
parent f3414f23f6
commit 507e02db54
4 changed files with 77 additions and 25 deletions

BIN
diff.txt Normal file

Binary file not shown.

View File

@@ -77,11 +77,11 @@ export function WizardControls({
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`) console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
const data = await generatePlanAI.mutateAsync(aiInput as any) const plan = await generatePlanAI.mutateAsync(aiInput as any)
console.log(`${new Date().toISOString()} - Plan IA generado`, data) console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
navigate({ navigate({
to: `/planes/${data.plan.id}`, to: `/planes/${plan.id}`,
state: { showConfetti: true }, state: { showConfetti: true },
}) })
return return
@@ -122,7 +122,7 @@ export function WizardControls({
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}> <Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
Anterior Anterior
</Button> </Button>
<div className="flex-1"> <div className="mx-2 flex-1">
{errorMessage && ( {errorMessage && (
<span className="text-destructive text-sm font-medium"> <span className="text-destructive text-sm font-medium">
{errorMessage} {errorMessage}

View File

@@ -268,8 +268,8 @@ export type AIGeneratePlanInput = {
estructuraPlanId: UUID estructuraPlanId: UUID
} }
iaConfig: { iaConfig: {
descripcionEnfoque: string descripcionEnfoqueAcademico: string
notasAdicionales?: string instruccionesAdicionalesIA?: string
archivosReferencia?: Array<UUID> archivosReferencia?: Array<UUID>
repositoriosIds?: Array<UUID> repositoriosIds?: Array<UUID>
archivosAdjuntos: Array<UploadedFile> archivosAdjuntos: Array<UploadedFile>

View File

@@ -1,12 +1,18 @@
import { supabaseBrowser } from "./client"; import {
FunctionsFetchError,
FunctionsHttpError,
FunctionsRelayError,
} from '@supabase/supabase-js'
import type { Database } from "@/types/supabase"; import { supabaseBrowser } from './client'
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from '@/types/supabase'
import type { SupabaseClient } from '@supabase/supabase-js'
export type EdgeInvokeOptions = { export type EdgeInvokeOptions = {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
headers?: Record<string, string>; headers?: Record<string, string>
}; }
export class EdgeFunctionError extends Error { export class EdgeFunctionError extends Error {
constructor( constructor(
@@ -15,8 +21,8 @@ export class EdgeFunctionError extends Error {
public readonly status?: number, public readonly status?: number,
public readonly details?: unknown, public readonly details?: unknown,
) { ) {
super(message); super(message)
this.name = "EdgeFunctionError"; this.name = 'EdgeFunctionError'
} }
} }
@@ -34,23 +40,69 @@ export async function invokeEdge<TOut>(
opts: EdgeInvokeOptions = {}, opts: EdgeInvokeOptions = {},
client?: SupabaseClient<Database>, client?: SupabaseClient<Database>,
): Promise<TOut> { ): Promise<TOut> {
const supabase = client ?? supabaseBrowser(); const supabase = client ?? supabaseBrowser()
const { data, error } = await supabase.functions.invoke(functionName, { const { data, error } = await supabase.functions.invoke(functionName, {
body, body,
method: opts.method ?? "POST", method: opts.method ?? 'POST',
headers: opts.headers, headers: opts.headers,
}); })
if (error) { if (error) {
const anyErr = error; // Valores por defecto (por si falla el parseo o es otro tipo de error)
throw new EdgeFunctionError( let message = error.message // El genérico "returned a non-2xx status code"
anyErr.message ?? "Error en Edge Function", let status = undefined
functionName, let details: unknown = error
anyErr.status,
anyErr, // 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
} }