fix: guard against Pydantic validation error objects in toast/error messages
FastAPI returns `detail` as an array of objects for 422 validation errors, not a string. Passing these objects to toast.error() or rendering them in JSX crashes React with Error #31 ("Objects are not valid as a React child"). Now checks typeof detail === 'string' before using it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,12 @@ function handleGlobalError(error: AxiosError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const status = error.response.status
|
const status = error.response.status
|
||||||
const data = error.response.data as { detail?: string }
|
const data = error.response.data as { detail?: string | unknown[] }
|
||||||
|
|
||||||
|
// Extract a displayable error message from the response.
|
||||||
|
// FastAPI returns `detail` as a string for most errors, but 422 validation
|
||||||
|
// errors return an array of objects — we must not pass those to toast.
|
||||||
|
const detail = typeof data?.detail === 'string' ? data.detail : undefined
|
||||||
|
|
||||||
// Don't show toast for 401 (handled by refresh interceptor)
|
// Don't show toast for 401 (handled by refresh interceptor)
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
@@ -34,13 +39,13 @@ function handleGlobalError(error: AxiosError) {
|
|||||||
|
|
||||||
// Rate limit
|
// Rate limit
|
||||||
if (status === 429) {
|
if (status === 429) {
|
||||||
toast.error(data?.detail || 'Too many requests — please try again shortly')
|
toast.error(detail || 'Too many requests — please try again shortly')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client errors (4xx) — show backend detail if present
|
// Client errors (4xx) — show backend detail if present
|
||||||
if (status >= 400 && status < 500) {
|
if (status >= 400 && status < 500) {
|
||||||
toast.error(data?.detail || 'Invalid request')
|
toast.error(detail || 'Invalid request')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
// Fetch user info
|
// Fetch user info
|
||||||
await get().fetchUser()
|
await get().fetchUser()
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const axiosErr = error as { response?: { data?: { detail?: string } } }
|
const axiosErr = error as { response?: { data?: { detail?: unknown } } }
|
||||||
const message = axiosErr.response?.data?.detail || (error instanceof Error ? error.message : 'Login failed')
|
const rawDetail = axiosErr.response?.data?.detail
|
||||||
|
const message = (typeof rawDetail === 'string' ? rawDetail : null) || (error instanceof Error ? error.message : 'Login failed')
|
||||||
set({ error: message, isLoading: false })
|
set({ error: message, isLoading: false })
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -62,8 +63,9 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
// After registration, log the user in
|
// After registration, log the user in
|
||||||
await get().login({ email: data.email, password: data.password })
|
await get().login({ email: data.email, password: data.password })
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const axiosErr = error as { response?: { data?: { detail?: string } } }
|
const axiosErr = error as { response?: { data?: { detail?: unknown } } }
|
||||||
const message = axiosErr.response?.data?.detail || (error instanceof Error ? error.message : 'Registration failed')
|
const rawDetail = axiosErr.response?.data?.detail
|
||||||
|
const message = (typeof rawDetail === 'string' ? rawDetail : null) || (error instanceof Error ? error.message : 'Registration failed')
|
||||||
set({ error: message, isLoading: false })
|
set({ error: message, isLoading: false })
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user