{"record":{"id":"adb3e18aa7cb38f6","repo":"yikart/AiToEarn","slug":"save-failed","errorCode":null,"errorMessage":"Save failed","messagePattern":"Save failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"project/aitoearn-web/src/app/[lng]/chat/[taskId]/page.tsx","lineNumber":322,"sourceCode":"    catch {\n      setIsFavorited(!newValue) // 失败回滚\n      toast.error(t('message.error'))\n    }\n  }, [isFavorited, taskId, t])\n\n  /**\n   * 保存标题 - 乐观更新\n   */\n  const handleSaveTitle = useCallback(\n    async (newTitle: string) => {\n      const result = await agentApi.updateTaskTitle(taskId, newTitle)\n      if (result && result.code === 0) {\n        // 更新本地 task 状态，触发页面标题和 header 标题更新\n        updateTaskTitle(newTitle)\n        toast.success(t('task.titleUpdated'))\n      }\n      else {\n        throw new Error('Save failed')\n      }\n    },\n    [taskId, t, updateTaskTitle],\n  )\n\n  // 加载中状态（仅非活跃任务显示骨架屏）\n  if (isLoading && !isActiveTask) {\n    return <ChatLoadingSkeleton />\n  }\n\n  return (\n    <div className=\"flex flex-col h-full\">\n      {/* 顶部导航 */}\n      <ChatHeader\n        title={task?.title}\n        defaultTitle={t('task.newChat')}\n        isGenerating={isGenerating}\n        progress={progress}","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-web/src/app/[lng]/chat/[taskId]/page.tsx#L304-L340","documentation":"In ChatDetailPage, saving a renamed task calls the update-title API and expects result.code === 0. When the response code is non-zero (or result is falsy), it throws Error('Save failed') after skipping the local title update.","triggerScenarios":"The rename-task API returns { code: <non-zero> } (business error: invalid taskId, permission denied, validation failure) or returns no parsable result — even when the HTTP status was 200.","commonSituations":"Task already deleted in another tab (stale taskId), session/token expired so the backend returns an error envelope with HTTP 200, empty or invalid new title rejected by validation, or network layer returning an undefined result.","solutions":["Log the full result object (code + message) to see the backend's business error reason","Refresh/re-fetch the task to confirm taskId is still valid, then retry","Re-authenticate if the error envelope indicates auth failure (401-style code with HTTP 200)","Validate the new title (non-empty, length limits) before calling the API and surface the API's message instead of the generic 'Save failed'"],"exampleFix":"// before\nelse {\n  throw new Error('Save failed')\n}\n// after\nelse {\n  throw new Error(`Save failed: ${result?.message ?? 'unknown error'} (code: ${result?.code})`)\n}","handlingStrategy":"try-catch","validationCode":"const newTitle = title.trim()\nif (!newTitle || newTitle.length > 100) { toast.error('标题不能为空且需小于100字'); return }\nif (!taskId) return","typeGuard":"function isApiSuccess(result: unknown): result is { code: 0; data?: unknown } {\n  return typeof result === 'object' && result !== null && (result as any).code === 0\n}","tryCatchPattern":"try {\n  await saveTitle(newTitle)\n} catch (e) {\n  if (e.message === 'Save failed') {\n    toast.error('保存失败，请刷新页面后重试')\n    await refetchTask(taskId) // resync local state with server\n    return\n  }\n  throw e\n}","preventionTips":["Include the API's code/message in the thrown error for diagnosability","Re-validate taskId against the server before mutations (task may be deleted elsewhere)","Handle auth-expiry globally so error envelopes with HTTP 200 trigger re-login","Trim/validate title input before sending"],"tags":["api","business-error","chat","save"],"backgroundTag":"api-business-error-code","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}