{"record":{"id":"90c3dc8f63983838","repo":"odysseus-dev/odysseus","slug":"http-res-status","errorCode":null,"errorMessage":"HTTP ${res.status}","messagePattern":"HTTP \\$\\{res\\.status\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"static/js/chat.js","lineNumber":6417,"sourceCode":"      bodyEl.appendChild(_rwSpin.element);\n    }\n    // Stop + detach the spinner (called once real content starts rendering, and\n    // on the failure path so it never spins forever).\n    const _killRwSpin = () => { if (_rwSpin) { try { _rwSpin.destroy(); } catch (_) {} _rwSpin = null; } };\n\n    try {\n      const res = await fetch(`${API_BASE}/api/rewrite`, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          session_id: sessionId,\n          original_text: oldRaw,\n          instruction: instruction,\n        }),\n      });\n\n      if (!res.ok) {\n        throw new Error(`HTTP ${res.status}`);\n      }\n\n      const reader = res.body.getReader();\n      const decoder = new TextDecoder();\n      let buffer = '';\n      let newText = '';\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        buffer += decoder.decode(value, { stream: true });\n        const lines = buffer.split('\\n');\n        buffer = lines.pop() || '';\n\n        for (const line of lines) {\n          if (!line.startsWith('data: ')) continue;\n          const payload = line.slice(6).trim();\n          if (payload === '[DONE]') continue;","sourceCodeStart":6399,"sourceCodeEnd":6435,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/chat.js#L6399-L6435","documentation":"HTTP 400 from PUT /{file_id}/vision when the parsed JSON body's 'text' field is present but not a string (its default when absent is \"\", which passes). The route deliberately validates types itself because it reads the raw Request rather than a Pydantic model — numbers, arrays, objects, or booleans in 'text' all trigger this.","triggerScenarios":"PUT with {\"text\": 123}, {\"text\": null} (None is not str in Python's isinstance), {\"text\": [\"a\",\"b\"]}, or {\"text\": {\"body\": \"...\"}} — e.g. a client wrapping the text one level too deep.","commonSituations":"Client sends a number from an untyped input; a refactor changes the payload shape from string to {text, lang} object without updating this endpoint's expectation; null sent to 'clear' the text instead of an empty string.","solutions":["Send text as a JSON string: {\"text\": \"the OCR text\"}.","To clear text, send {\"text\": \"\"} (empty string), not null.","Client-side, coerce input with String(value) before serializing.","Update the API client to the documented payload shape — nested objects are not unwrapped here."],"exampleFix":"// before\nbody: JSON.stringify({ text: { content: editedText } })\n\n// after\nbody: JSON.stringify({ text: editedText })","handlingStrategy":"type-guard","validationCode":"if (typeof text !== 'string') text = String(text ?? ''); // coerce before send","typeGuard":"function isVisionPayload(b: unknown): b is { text: string } {\n  return typeof b === 'object' && b !== null && typeof (b as any).text === 'string';\n}","tryCatchPattern":"const r = await saveVision(id, text);\nif (r.status === 400 && (await r.text()).includes('string')) alert('Text must be a plain string');","preventionTips":["Send {\"text\": \"...\"} — plain string, not nested objects or numbers","Use an empty string to clear, never null","Type the payload (TS interface or Pydantic model) on the client side"],"tags":["http-400","type-validation","json","vision"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}