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

  1. Send text as a JSON string: {"text": "the OCR text"}.
  2. To clear text, send {"text": ""} (empty string), not null.
  3. Client-side, coerce input with String(value) before serializing.
  4. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/90c3dc8f63983838. Report an issue: GitHub.