odysseus-dev/odysseus · error · Error
Document create failed: missing id
Error message
Document create failed: missing id
What it means
Thrown in static/js/document.js when POST /api/document returns 2xx but the JSON body has no id (falsy doc or doc.id). It guards addDocToTabs/switchToDoc from receiving an unusable document record — a response-shape contract violation rather than an HTTP failure.
Source
Thrown at static/js/document.js:7046
// If the panel was in empty-state, the user may type into the editor
// during the create round-trip — preserve that text into the new doc
// instead of letting switchToDoc blank it.
const wasEmpty = !activeDocId;
try {
const res = await fetch(`${API_BASE}/api/document`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
session_id: sessionId,
title: '',
content: '',
language: 'markdown',
}),
});
if (!res.ok) throw new Error(`Document create failed: HTTP ${res.status}`);
const doc = await res.json();
if (!doc || !doc.id) throw new Error('Document create failed: missing id');
addDocToTabs(doc, sessionId);
if (!isOpen) openPanel();
// Re-enable editor if it was in empty state
let textarea = document.getElementById('doc-editor-textarea');
if (textarea) {
textarea.disabled = false;
textarea.placeholder = 'Document content...';
}
// Capture text typed during the round-trip (only when starting from the
// empty editor — don't steal another doc's content).
const typed = (wasEmpty && textarea && textarea.value.trim()) ? textarea.value : '';
switchToDoc(doc.id);
if (typed) {
textarea = document.getElementById('doc-editor-textarea');
if (textarea) textarea.value = typed;
const d = docs.get(doc.id);
if (d) d.content = typed;
syncHighlighting();View on GitHub (pinned to f9235ebbf1)
Solutions
- Inspect the actual 200 response body for POST /api/document.
- If the field was renamed, accept the alias here like the import path does (doc.id || doc.doc_id) or fix the backend to return id.
- Ensure no response-transforming middleware wraps the payload.
Example fix
// before
const doc = await res.json();
if (!doc || !doc.id) throw new Error('Document create failed: missing id');
// after
const doc = await res.json();
const newId = doc && (doc.id || doc.doc_id);
if (!newId) throw new Error(`Document create failed: missing id (keys: ${Object.keys(doc || {}).join(', ')})`);
doc.id = newId; Defensive patterns
Strategy: type-guard
Type guard
function hasDocumentId(doc) {
return doc != null && typeof doc === 'object'
&& (typeof doc.id === 'string' || typeof doc.id === 'number')
&& String(doc.id).length > 0;
} Try / catch
const doc = await res.json();
if (!hasDocumentId(doc) && !doc?.doc_id) {
throw new Error(`Document create failed: missing id (got keys: ${Object.keys(doc || {}).join(', ')})`);
} Prevention
- Accept the id/doc_id alias like the import path does, or normalize the backend to always return id.
- Contract-test the create endpoint's 200 body shape in CI.
- Watch for middleware that wraps responses in {data: ...}.
When it happens
Trigger: Backend returns {ok: true} or an empty object instead of the created document; a proxy or interceptor rewrites the response; backend refactor renamed id to doc_id (note the sibling import path at document.js:9584 accepts both j.id || j.doc_id, so this site is stricter).
Common situations: API version drift where the create endpoint's response schema changed; middleware returning 200 with a wrapper object {data: {...}}.
Related errors
- No uploaded files returned
- No draft id returned
- d && d.detail ? d.detail : ('HTTP ' + res.status)
- result.detail || 'Failed to rename session'
- Invalid backup file: ' + e.message
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/40b60e6eba88d68d.
Report an issue: GitHub.