odysseus-dev/odysseus · warning · Error
HTTP ${res.status}
Error message
HTTP ${res.status} What it means
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.
Source
Thrown at static/js/chat.js:6417
bodyEl.appendChild(_rwSpin.element);
}
// Stop + detach the spinner (called once real content starts rendering, and
// on the failure path so it never spins forever).
const _killRwSpin = () => { if (_rwSpin) { try { _rwSpin.destroy(); } catch (_) {} _rwSpin = null; } };
try {
const res = await fetch(`${API_BASE}/api/rewrite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
original_text: oldRaw,
instruction: instruction,
}),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let newText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const payload = line.slice(6).trim();
if (payload === '[DONE]') continue;View on GitHub (pinned to f9235ebbf1)
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.
Example fix
// before
body: JSON.stringify({ text: { content: editedText } })
// after
body: JSON.stringify({ text: editedText }) Defensive patterns
Strategy: type-guard
Validate before calling
if (typeof text !== 'string') text = String(text ?? ''); // coerce before send
Type guard
function isVisionPayload(b: unknown): b is { text: string } {
return typeof b === 'object' && b !== null && typeof (b as any).text === 'string';
} Try / catch
const r = await saveVision(id, text);
if (r.status === 400 && (await r.text()).includes('string')) alert('Text must be a plain string'); Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- await res.text()
- Invalid JSON
- Expected a JSON object
- HTTP ' + r.status
- d.error || 'Failed to create calendar'
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/90c3dc8f63983838.
Report an issue: GitHub.