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
- Confirm the id exists by downloading it (GET /{file_id}) — if download also fails, the upload is gone or the index lost it.
- If the index is the problem, restore uploads.json from its .bak sibling or rebuild it; the file bytes are likely still on disk.
- Handle 404 gracefully in the editor UI (drop the stale attachment) instead of retrying.
- 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
- Check the attachment still downloads before showing the OCR editor
- Handle 404 on save by discarding the stale attachment state
- Maintain uploads.json and its .bak so index entries survive crashes
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
- Server error ' + res.status
- HTTP ' + r.status
- d.error || 'Failed to create calendar'
- await res.text()
- HTTP ${res.status}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/103cecdd752fcb8d.
Report an issue: GitHub.