odysseus-dev/odysseus · warning · Error

Stream closed before completion

Error message

Stream closed before completion

What it means

HTTP 404 from PUT /{file_id}/vision when _load_upload_info(file_id) returns falsy — the id passes format validation but has no entry in the upload index (uploads.json). Unlike the GET routes, this route treats 'no index entry' as 404 before auth, because there is nothing to attach edited OCR text to. Note the index loader tolerates a corrupt uploads.json by falling back to .bak then {} — so a fully lost index makes every id 404 here.

Source

Thrown at static/js/chat.js:3931

                const errDiv = document.createElement('div');
                errDiv.style.cssText = 'color: var(--color-error); font-style: italic; padding: 4px 0;';
                errDiv.textContent = `[Error: ${json.error}]`;
                roundHolder.querySelector('.body').appendChild(errDiv);
                uiModule.scrollHistory();
              }
            } catch (e) {
              console.error('Error parsing SSE data:', e);
            }
          }
        }
      }

      if (_streamTerminalError) {
        throw _streamTerminalError;
      }
      if (!_streamSawDone) {
        if (!_canonicalTerminalSaved) {
          throw new Error('Stream closed before completion');
        }
        // The backend persisted a canonical terminal record (partial output +
        // failure metadata) before the connection died. Route through the
        // terminal-error path so that record is reloaded; falling through to
        // the success renderer would present the partial output as a clean
        // completion.
        throw createTerminalStreamError({
          text: 'Stream closed after canonical terminal event',
        });
      }

      // The final foreground render below is authoritative. Cancel any delayed
      // live-view work instead of parsing and rendering the full round once
      // here and then immediately replacing it.
      _cancelLiveThinkingWork();
      if (spinner && spinner.element) { try { spinner.destroy(); } catch (_) {} spinner = null; }
      _cancelThinkingTimer();
      _removeThinkingSpinner();

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Confirm the id exists by downloading it (GET /{file_id}) — if download also fails, the upload is gone or the index lost it.
  2. If the index is the problem, restore uploads.json from its .bak sibling or rebuild it; the file bytes are likely still on disk.
  3. Handle 404 gracefully in the editor UI (drop the stale attachment) instead of retrying.
  4. Re-upload the image if the underlying file was legitimately cleaned up, then re-enter the OCR text.
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the upload still exists before offering OCR editing
const head = await fetch(`/api/upload/${id}`, { method: 'GET', credentials: 'include' });
if (!head.ok) markAttachmentStale(id);

Try / catch

try {
  const r = await fetch(`/api/upload/${id}/vision`, { method: 'PUT', credentials: 'include', ... });
  if (r.status === 404) { dropStaleAttachment(id); return; } // file/index gone — stop editing
} catch (e) { /* network handling */ }

Prevention

When it happens

Trigger: PUT /api/upload/{valid-hex-id}/vision for an id never uploaded, deleted by cleanup, or whose index entry was lost when uploads.json was truncated with no usable .bak.

Common situations: Client retries an edit after the file was cleaned up by the hourly/periodic cleanup; uploads.json corrupted on crash and both primary and .bak unreadable; a stale front-end cache referencing old attachment ids after a data reset.

Related errors


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