feat: implement error handling for task preview generation and add error message utility
All checks were successful
Build And Publish Production Image / Build And Publish Production Image (push) Successful in 14s

This commit is contained in:
2026-03-28 16:21:03 -03:00
parent 9b554644bc
commit f5ef46d91a
5 changed files with 128 additions and 10 deletions

View File

@@ -328,4 +328,18 @@ INSTRUCTIONS
'backend unavailable'
)
})
it('should_returnReadable401Message_when_generateTaskPreviewUnauthorized', async () => {
mockedApiClient.post.mockRejectedValue({
isAxiosError: true,
response: {
status: 401,
data: {},
},
})
await expect(generateTaskPreview('task-1', { entity, task: previewTask })).rejects.toThrow(
'Your session has expired. Please sign in again and retry.'
)
})
})

View File

@@ -205,7 +205,7 @@ describe('EditTaskPage', () => {
it('should_showReadablePreviewError_when_generationFails', async () => {
vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue(
new Error('Unable to generate a test message from the local model. Connection refused')
new Error('Your session has expired. Please sign in again and retry.')
)
renderPage()
@@ -215,7 +215,7 @@ describe('EditTaskPage', () => {
await waitFor(() => {
expect(
screen.getByText(/Unable to generate a test message from the local model. Connection refused/i)
screen.getByText(/Your session has expired. Please sign in again and retry./i)
).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,53 @@
import { isAxiosError } from 'axios'
function getMessageFromResponseData(data: unknown): string | null {
if (!data || typeof data !== 'object') {
return null
}
const payload = data as Record<string, unknown>
const candidate = payload.message ?? payload.error ?? payload.detail
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim()
}
return null
}
export function getApiErrorMessage(error: unknown, fallback: string): string {
if (isAxiosError(error)) {
const status = error.response?.status
const responseMessage = getMessageFromResponseData(error.response?.data)
if (responseMessage) {
return responseMessage
}
if (status === 401) {
return 'Your session has expired. Please sign in again and retry.'
}
if (status === 403) {
return 'You do not have permission to perform this action.'
}
if (status === 404) {
return 'The requested resource was not found.'
}
if (status && status >= 500) {
return 'The server failed to process the request. Please try again in a moment.'
}
if (!error.response) {
return 'Unable to reach the server. Check your network connection and try again.'
}
}
if (error instanceof Error && error.message.trim().length > 0) {
return error.message
}
return fallback
}

View File

@@ -1,5 +1,6 @@
import type { VirtualEntityResponse } from './entitiesApi'
import apiClient from './apiClient'
import { getApiErrorMessage } from './errorMessage'
export type EmailLookback = 'last_day' | 'last_week' | 'last_month'
export type GenerationSource = 'openai' | 'llama'
@@ -88,11 +89,17 @@ export async function generateTaskPreview(
taskId: string,
payload: TaskPreviewRequest
): Promise<string> {
const response = await apiClient.post<GeneratedMessageHistoryItem>(
`/v1/tasks/${taskId}/generated-messages/generate`,
payload
)
return response.data.content
try {
const response = await apiClient.post<GeneratedMessageHistoryItem>(
`/v1/tasks/${taskId}/generated-messages/generate`,
payload
)
return response.data.content
} catch (error) {
throw new Error(
getApiErrorMessage(error, 'Unable to generate a test message right now. Please try again.')
)
}
}
/** Returns generated message history for one task. */

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { getApiErrorMessage } from '../api/errorMessage'
import { getEntity } from '../api/entitiesApi'
import {
activateTask,
@@ -122,6 +123,7 @@ export default function EditTaskPage() {
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
const [selectedMessageId, setSelectedMessageId] = useState('')
const [previewError, setPreviewError] = useState('')
const [actionError, setActionError] = useState('')
const { data: entity, isLoading: isLoadingEntity } = useQuery({
queryKey: ['entity', entityId],
@@ -181,34 +183,58 @@ export default function EditTaskPage() {
emailLookback: data.emailLookback,
generationSource: data.generationSource,
}),
onMutate: () => {
setActionError('')
},
onSuccess: async () => {
await invalidateTaskQueries(queryClient, entityId, taskId)
navigate(`/entities/${entityId}`)
},
onError: (error) => {
setActionError(getApiErrorMessage(error, 'Unable to save the task right now. Please try again.'))
},
})
const inactivateTaskMutation = useMutation({
mutationFn: () => inactivateTask(taskId),
onMutate: () => {
setActionError('')
},
onSuccess: async () => {
await invalidateTaskQueries(queryClient, entityId, taskId)
navigate(`/entities/${entityId}`)
},
onError: (error) => {
setActionError(getApiErrorMessage(error, 'Unable to inactivate the task right now. Please try again.'))
},
})
const activateTaskMutation = useMutation({
mutationFn: () => activateTask(taskId),
onMutate: () => {
setActionError('')
},
onSuccess: async () => {
await invalidateTaskQueries(queryClient, entityId, taskId)
navigate(`/entities/${entityId}`)
},
onError: (error) => {
setActionError(getApiErrorMessage(error, 'Unable to activate the task right now. Please try again.'))
},
})
const deleteTaskMutation = useMutation({
mutationFn: () => deleteTask(taskId),
onMutate: () => {
setActionError('')
},
onSuccess: async () => {
await invalidateTaskQueries(queryClient, entityId, taskId)
navigate(`/entities/${entityId}`)
},
onError: (error) => {
setActionError(getApiErrorMessage(error, 'Unable to delete the task right now. Please try again.'))
},
})
const previewMutation = useMutation({
@@ -222,9 +248,10 @@ export default function EditTaskPage() {
},
onError: (error) => {
setPreviewError(
error instanceof Error
? error.message
: 'Unable to generate a test message from the local model.'
getApiErrorMessage(
error,
'Unable to generate a test message right now. Please try again.'
)
)
},
})
@@ -268,6 +295,9 @@ export default function EditTaskPage() {
const deleteGeneratedMessageMutation = useMutation({
mutationFn: (messageId: string) => deleteTaskGeneratedMessage(taskId, messageId),
onMutate: () => {
setActionError('')
},
onSuccess: ( _data, messageId) => {
queryClient.setQueryData(
['task-generated-messages', taskId],
@@ -278,6 +308,14 @@ export default function EditTaskPage() {
) => currentMessages?.filter((message) => message.id !== messageId) ?? []
)
},
onError: (error) => {
setActionError(
getApiErrorMessage(
error,
'Unable to delete the generated message right now. Please try again.'
)
)
},
})
const applyCronParts = (nextCronParts: CronParts) => {
@@ -589,6 +627,12 @@ export default function EditTaskPage() {
</div>
{actionError && (
<p role="alert" className="text-sm text-red-300">
{actionError}
</p>
)}
<div className="flex justify-end gap-3 pb-8">
<button
type="button"