{"record":{"id":"9485e09d6bb53369","repo":"odysseus-dev/odysseus","slug":"no-uploaded-files-returned","errorCode":null,"errorMessage":"No uploaded files returned","messagePattern":"No uploaded files returned","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/document.js","lineNumber":3573,"sourceCode":"    }\n    if (_activeDocLanguage() !== 'markdown') {\n      if (uiModule) uiModule.showError('Switch the document to markdown before inserting images');\n      return;\n    }\n\n    const fd = new FormData();\n    images.forEach(file => fd.append('files', file));\n    try {\n      const res = await fetch(`${API_BASE}/api/upload`, {\n        method: 'POST',\n        credentials: 'same-origin',\n        body: fd,\n      });\n      let data = null;\n      try { data = await res.json(); } catch (_) {}\n      if (!res.ok) throw new Error((data && (data.error || data.detail)) || `HTTP ${res.status}`);\n      const uploaded = Array.isArray(data?.files) ? data.files : [];\n      if (!uploaded.length) throw new Error('No uploaded files returned');\n      _insertMarkdownImages(uploaded);\n      if (uiModule) uiModule.showToast(images.length === 1 ? 'Image inserted' : 'Images inserted');\n    } catch (err) {\n      console.error('Failed to insert markdown image:', err);\n      if (uiModule) uiModule.showError('Failed to insert image');\n    }\n  }\n\n  async function _handleMarkdownImageUpload(e) {\n    const files = e.target.files;\n    e.target.value = '';\n    await _uploadMarkdownImages(files);\n  }\n\n  function _renderComposeAttachments() {\n    const container = document.getElementById('doc-email-compose-atts');\n    if (!container) return;\n    const doc = docs.get(activeDocId);","sourceCodeStart":3555,"sourceCodeEnd":3591,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/document.js#L3555-L3591","documentation":"Thrown in static/js/document.js after a successful POST /api/upload when the parsed JSON has no non-empty files array (data.files is missing or empty). It is a contract violation: the upload endpoint returned success but not the expected {files: [...]} shape, so there is nothing to insert into the markdown.","triggerScenarios":"Backend version that returns {urls: [...]} or {uploaded: [...]} instead of files; endpoint responding with {success: true} but zero saved files (e.g. all files filtered server-side); response actually an empty object because a proxy stripped the body.","commonSituations":"Frontend/backend drift after an API refactor renamed the response field; backend silently dropping files that fail validation while still returning 200.","solutions":["Log/inspect the actual response body of POST /api/upload to see which field carries the uploaded file descriptors.","Update the backend to return files: [...] or the frontend to read the correct field.","Make the backend return a non-2xx status when zero files were stored instead of an empty 200."],"exampleFix":"// before\nconst uploaded = Array.isArray(data?.files) ? data.files : [];\nif (!uploaded.length) throw new Error('No uploaded files returned');\n\n// after — accept the documented aliases, fail loudly otherwise\nconst uploaded = Array.isArray(data?.files) ? data.files\n  : Array.isArray(data?.uploaded) ? data.uploaded : [];\nif (!uploaded.length) throw new Error(`Upload returned no files (payload keys: ${Object.keys(data || {}).join(', ')})`);","handlingStrategy":"validation","validationCode":"if (!images.length) return; // don't upload nothing and expect files back","typeGuard":"function hasUploadedFiles(data) {\n  return data != null && typeof data === 'object'\n    && Array.isArray(data.files) && data.files.length > 0\n    && data.files.every(f => f && (f.url || f.path));\n}","tryCatchPattern":"if (!hasUploadedFiles(data)) {\n  console.error('Unexpected upload response shape:', data);\n  if (uiModule) uiModule.showError('Upload succeeded but returned no files');\n  return;\n}","preventionTips":["Pin the /api/upload response contract ({files: [{url, ...}]}) in a shared types/schema file used by both sides.","Add an integration test asserting the upload response contains a non-empty files array.","Log the response keys on shape mismatch to catch API drift immediately."],"tags":["api-contract","file-upload","response-shape","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}