{"record":{"id":"bdf897173fca40ed","repo":"666ghj/MiroFish","slug":"res-error-t-step5-requestfailed","errorCode":null,"errorMessage":"res.error || t('step5.requestFailed')","messagePattern":"res\\.error \\|\\| t\\('step5\\.requestFailed'\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/src/components/Step5Interaction.vue","lineNumber":708,"sourceCode":"      role: msg.role,\n      content: msg.content\n    }))\n  \n  const res = await chatWithReport({\n    simulation_id: props.simulationId,\n    message: message,\n    chat_history: historyForApi\n  })\n  \n  if (res.success && res.data) {\n    chatHistory.value.push({\n      role: 'assistant',\n      content: res.data.response || res.data.answer || t('step5.noResponse'),\n      timestamp: new Date().toISOString()\n    })\n    addLog(t('log.reportAgentReplied'))\n  } else {\n    throw new Error(res.error || t('step5.requestFailed'))\n  }\n}\n\nconst sendToAgent = async (message) => {\n  if (!selectedAgent.value || selectedAgentIndex.value === null) {\n    throw new Error(t('step5.selectAgentFirst'))\n  }\n  \n  addLog(t('log.sendToAgent', { name: selectedAgent.value.username, message: message.substring(0, 50) }))\n  \n  // Build prompt with chat history\n  let prompt = message\n  if (chatHistory.value.length > 1) {\n    const historyContext = chatHistory.value\n      .slice(0, -1)\n      .slice(-6)\n      .map(msg => `${msg.role === 'user' ? '提问者' : '你'}：${msg.content}`)\n      .join('\\n')","sourceCodeStart":690,"sourceCodeEnd":726,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/frontend/src/components/Step5Interaction.vue#L690-L726","documentation":"Thrown by sendToReportAgent in Step5Interaction.vue when the backend call to chatWithReport (POST with simulation_id, message, chat_history) resolves with success=false or an empty data payload. This is the app's own request-wrapper convention: any non-2xx or business failure comes back as { success: false, error }, and the component converts that into a thrown Error so sendMessage's catch block can render it in the chat log. The i18n key step5.requestFailed is only the fallback text when the server gave no error string.","triggerScenarios":"Calling chatWithReport({ simulation_id, message, chat_history }) when: the simulation_id does not exist or has expired server-side; the report-generation Celery/LLM task failed; the backend returned 4xx/5xx that the request wrapper flattens into { success: false }; or the response succeeded but data was null/undefined.","commonSituations":"Session/simulation no longer running (page left open past backend TTL), report agent pipeline crashed mid-generation, backend restarted between page load and chat send, or a proxy/gateway (502/504) intercepting the request during LLM long-polls.","solutions":["Inspect the res.error value and the browser Network tab response for the chatWithReport call to get the real backend reason.","Verify props.simulationId matches an existing, still-active simulation (check the backend simulation listing endpoint).","Check backend logs for the report-agent chat route for stack traces (LLM provider errors, missing API keys, timeouts).","If it is an intermittent timeout from a slow LLM, increase the request timeout for the chat endpoint in the API client.","If the server returns no error field, improve the backend route to always populate error on failure so users see the true cause instead of the generic i18n fallback."],"exampleFix":"// before\nconst res = await chatWithReport({ simulation_id: props.simulationId, message, chat_history: historyForApi })\nif (res.success && res.data) { /* ... */ } else {\n  throw new Error(res.error || t('step5.requestFailed'))\n}\n\n// after - surface HTTP status detail and rethrow only once\nconst res = await chatWithReport({ simulation_id: props.simulationId, message, chat_history: historyForApi })\nif (!res.success) {\n  throw new Error(res.error || `Chat request failed (HTTP ${res.status ?? 'unknown'})`)\n}\nif (!res.data) {\n  throw new Error(t('step5.noResponse'))\n}\nchatHistory.value.push({ role: 'assistant', content: res.data.response || res.data.answer || t('step5.noResponse'), timestamp: new Date().toISOString() })","handlingStrategy":"try-catch","validationCode":"if (!props.simulationId) {\n  addLog(t('log.invalidSimulation'))\n  return\n}\nconst historyForApi = chatHistory.value.slice(0, -1).slice(-10).map(m => ({ role: m.role, content: m.content }))\nif (historyForApi.some(m => !m.content)) {\n  chatHistory.value = chatHistory.value.filter(m => m.content)\n}","typeGuard":"function isChatSuccess(res) {\n  return Boolean(res && res.success === true && res.data && (res.data.response || res.data.answer))\n}","tryCatchPattern":"try {\n  const res = await chatWithReport({ simulation_id: props.simulationId, message, chat_history: historyForApi })\n  if (!res.success || !res.data) throw new Error(res.error || t('step5.requestFailed'))\n  /* push assistant message */\n} catch (err) {\n  addLog(t('log.sendFailed', { error: err.message }))\n  chatHistory.value.push({ role: 'assistant', content: t('step5.errorOccurred', { error: err.message }), timestamp: new Date().toISOString() })\n} finally {\n  isSending.value = false\n}","preventionTips":["Disable the send button while the simulation is not in a running state so requests cannot target dead simulations.","Always populate res.error on the backend failure envelope so users see real causes instead of the generic i18n fallback.","Keep a single catch in sendMessage (already present) and let sendToReportAgent only throw; do not catch twice.","Verify simulation status on page mount and prompt re-load if it expired."],"tags":["frontend","vue","api-error","chat","i18n"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}