{"record":{"id":"b6dfe7266e6b6880","repo":"odysseus-dev/odysseus","slug":"invalid-message-attachment-metadata","errorCode":null,"errorMessage":"Invalid message attachment metadata","messagePattern":"Invalid message attachment metadata","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/history/history_routes.py","lineNumber":119,"sourceCode":"\n\ndef setup_history_routes(session_manager, upload_handler=None) -> APIRouter:\n    router = APIRouter(tags=[\"history\"])\n\n    def _reserve_message_uploads(\n        request: Request,\n        content: Any,\n        metadata: Any = None,\n    ) -> None:\n        try:\n            missing_id = reserve_message_upload_references(\n                upload_handler,\n                effective_user(request),\n                content,\n                metadata,\n            )\n        except (TypeError, ValueError) as exc:\n            raise HTTPException(400, \"Invalid message attachment metadata\") from exc\n        if missing_id:\n            raise HTTPException(\n                409,\n                f\"Referenced upload is no longer available: {missing_id}\",\n            )\n\n    def _db_history_entry(m: DbChatMessage) -> Dict[str, Any]:\n        entry = {\"role\": m.role, \"content\": _history_display_content(m.content)}\n        meta = {}\n        if m.meta_data:\n            try:\n                meta = json.loads(m.meta_data) or {}\n            except (json.JSONDecodeError, ValueError):\n                meta = {}\n        if m.timestamp and \"timestamp\" not in meta:\n            meta[\"timestamp\"] = m.timestamp.isoformat() + \"Z\"\n        if meta:\n            entry[\"metadata\"] = meta","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/history/history_routes.py#L101-L137","documentation":"HTTP 400 raised by _reserve_message_uploads in history_routes when reserve_message_upload_references raises TypeError or ValueError. In src/upload_handler.py this happens when: metadata is a JSON string that fails json.loads (ValueError/JSONDecodeError) or is not None/str/dict-shaped (ValueError 'message metadata must be a JSON object'), or when its structure breaks attachment_refs_from_metadata (TypeError).","triggerScenarios":"POST /api/session/{id}/message with metadata as invalid JSON text ('{oops'), metadata as a list or number, or attachment refs array containing non-object entries.","commonSituations":"Client JSON.stringify-ing metadata twice (double-encoded string with stray quotes); sending FormData where metadata arrives as '[object Object]'; schema drift between client attachment format and attachment_refs_from_metadata expectations.","solutions":["Send metadata as a real JSON object (or omit it), never a JSON string with syntax errors","If you must send a string, ensure it is valid JSON and encodes an object","Shape attachments per the server's expected ref format: a list of objects each with an 'attachment_id' key"],"exampleFix":"// before (double-encoded / malformed)\nmetadata: JSON.stringify('{oops')\n\n// after\nmetadata: { attachments: [{ attachment_id: 'abc123.png' }] }","handlingStrategy":"type-guard","validationCode":"import json\ndef valid_metadata(md) -> bool:\n    if md in (None, ''):\n        return True\n    if isinstance(md, str):\n        try:\n            md = json.loads(md)\n        except ValueError:\n            return False\n    if not isinstance(md, dict):\n        return False\n    refs = md.get('attachments', [])\n    return all(isinstance(r, dict) and isinstance(r.get('attachment_id'), str) for r in refs)","typeGuard":"function isValidMetadata(m: unknown): boolean {\n  if (m === null || m === undefined || m === '') return true;\n  let obj = m;\n  if (typeof m === 'string') { try { obj = JSON.parse(m); } catch { return false; } }\n  if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false;\n  const refs = (obj as any).attachments;\n  return !Array.isArray(refs) || refs.every((r: any) => r && typeof r === 'object' && typeof r.attachment_id === 'string');\n}","tryCatchPattern":null,"preventionTips":["Send metadata as a native JSON object; never stringify it twice","Lint client serializers to catch '[object Object]' artifacts from FormData paths","Keep attachment ref objects shaped {attachment_id: string} and add a contract test"],"tags":["http-400","validation","metadata","attachments","uploads"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}