{"record":{"id":"c96fd3e21681945c","repo":"sigoden/aichat","slug":"failed-to-parse-messages-i","errorCode":null,"errorMessage":"Failed to parse '.messages[{i}]'","messagePattern":"Failed to parse '\\.messages\\[(.+?)\\]'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":400,"severity":"error","filePath":"src/serve.rs","lineNumber":820,"sourceCode":"\nfn ret_err<T: std::fmt::Display>(err: T) -> AppResponse {\n    let data = json!({\n        \"error\": {\n            \"message\": err.to_string(),\n            \"type\": \"invalid_request_error\",\n        },\n    });\n    Response::builder()\n        .header(\"Content-Type\", \"application/json\")\n        .body(Full::new(Bytes::from(data.to_string())).boxed())\n        .unwrap()\n}\n\nfn parse_messages(message: Vec<Value>) -> Result<Vec<Message>> {\n    let mut output = vec![];\n    let mut tool_results = None;\n    for (i, message) in message.into_iter().enumerate() {\n        let err = || anyhow!(\"Failed to parse '.messages[{i}]'\");\n        let role = message[\"role\"].as_str().ok_or_else(err)?;\n        let content = match message.get(\"content\") {\n            Some(value) => {\n                if let Some(value) = value.as_str() {\n                    MessageContent::Text(value.to_string())\n                } else if value.is_array() {\n                    let value = serde_json::from_value(value.clone()).map_err(|_| err())?;\n                    MessageContent::Array(value)\n                } else if value.is_null() {\n                    MessageContent::Text(String::new())\n                } else {\n                    return Err(err());\n                }\n            }\n            None => MessageContent::Text(String::new()),\n        };\n        match role {\n            \"system\" | \"user\" => {","sourceCodeStart":802,"sourceCodeEnd":838,"githubUrl":"https://github.com/sigoden/aichat/blob/82976d349ad97ac9aae0655ad631dace5e2a6385/src/serve.rs#L802-L838","documentation":"parse_messages (src/serve.rs:820) converts each raw JSON element of the `messages` array into the server's typed Message enum. When an element can't be matched to any Message variant — no `role` string, no recognizable `content`, or a tool_call/tool_result shape that fits no variant — it returns anyhow!(\"Failed to parse '.messages[{i}']\") with the failing index. chat_completions surfaces this as a 4xx 'Invalid request body' error.","triggerScenarios":"A messages[i] element lacking a string `role` (e.g. {\"content\": \"hi\"}), an unrecognized role value, content that is neither a string nor a supported content-part array (e.g. null, number, or an object), or a tool-calling message whose fields (tool_calls / tool_call_id / name) don't match any expected variant.","commonSituations":"Multi-turn clients appending assistant/tool messages in a format this server doesn't support (e.g. newer OpenAI content-part schemas, Anthropic-style blocks); history logs with null content entries; tool frameworks emitting provider-specific message shapes; truncation that drops the role field of the first message.","solutions":["Give every message a string `role` (\"system\"|\"user\"|\"assistant\"|\"tool\") and a `content` that is a plain string or the content-part array the server supports","Remove or repair null/empty content entries in conversation history","For tool flows, match the exact tool message shape parse_messages expects (see src/serve.rs:820) — usually {\"role\":\"tool\",\"tool_call_id\":\"...\",\"content\":\"...\"}","Read the index in the error (e.g. .messages[3]) and dump that specific element to see which field is malformed"],"exampleFix":"// before\n{\"messages\": [{\"role\": \"user\", \"content\": null}]}\n// after\n{\"messages\": [{\"role\": \"user\", \"content\": \"hello\"}]}","handlingStrategy":"validation","validationCode":"function validateMessageElement(m, i) {\n  const roles = ['system', 'user', 'assistant', 'tool'];\n  if (typeof m.role !== 'string' || !roles.includes(m.role)) return `.messages[${i}]: role must be one of ${roles.join('|')}`;\n  if (m.content == null) return `.messages[${i}]: content is required`;\n  if (typeof m.content !== 'string' && !Array.isArray(m.content)) return `.messages[${i}]: content must be a string or supported array`;\n  if (m.role === 'tool' && typeof m.tool_call_id !== 'string') return `.messages[${i}]: tool message requires tool_call_id`;\n  return null;\n}","typeGuard":"function isParsableMessage(m) {\n  const okContent = typeof m.content === 'string' || Array.isArray(m.content);\n  return typeof m === 'object' && m !== null\n    && typeof m.role === 'string' && okContent;\n}","tryCatchPattern":"payload.messages.forEach((m, i) => {\n  const e = validateMessageElement(m, i);\n  if (e) throw new Error(`Refusing to send: ${e} — got ${JSON.stringify(m)}`);\n});\ntry {\n  const res = await fetch('/chat_completions', {method:'POST', body: JSON.stringify(payload)});\n  if (!res.ok) throw new Error(await res.text());\n} catch (e) { /* handle */ }","preventionTips":["Normalize history from all sources (logs, DB, other providers) to the server's Message shape before sending","Drop or replace null-content messages instead of forwarding them","Use one typed message-builder helper across the codebase","When the server names .messages[i], log that element for immediate diagnosis"],"tags":["rust","json","chat-messages","validation","tool-calls"],"backgroundTag":"schema-validation-failed","analyzedSha":"82976d349ad97ac9aae0655ad631dace5e2a6385","analyzedAt":"2026-09-09T18:33:06.139Z","contentChangedAt":"2026-09-09T18:33:06.139Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}