sigoden/aichat · error · anyhow::Error
Invalid request body
Error message
Invalid request body, {err} What it means
In `search_rag` (src/serve.rs:243), the handler parses the raw request body into serde_json::Value successfully, then deserializes that Value into the `SearchRagReqBody { name, input }` struct. This error wraps serde's field/type error when the JSON has the correct syntax but does not match the struct: missing `name` or `input`, wrong types (e.g. input is a number instead of a string), or unexpected extra fields if deny_unknown_fields is set.
Solutions
- Include both required fields with correct types: `{"name": "<rag-file-name>", "input": "<query string>"}`
- Send `Content-Type: application/json` and a raw JSON body, not form data
- Check the exact field names and types of SearchRagReqBody in src/serve.rs and match your client payload to it
- Log the request body (the handler already debug!-logs it) and compare against the struct definition
Example fix
// before
curl -X POST /search_rag -d '{"rag_name": "docs", "query": "hello"}'
// after
curl -X POST /search_rag -H 'Content-Type: application/json' -d '{"name": "docs", "input": "hello"}' Defensive patterns
Strategy: validation
Validate before calling
function validateSearchRagBody(body) {
if (typeof body !== 'object' || body === null) return 'body must be an object';
if (typeof body.name !== 'string' || !body.name) return 'name (string) is required';
if (typeof body.input !== 'string' || !body.input) return 'input (string) is required';
return null;
} Type guard
function isSearchRagBody(v) {
return typeof v === 'object' && v !== null
&& typeof v.name === 'string'
&& typeof v.input === 'string';
} Try / catch
try {
const res = await fetch('/search_rag', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({name, input})});
if (!res.ok) {
const text = await res.text();
if (text.includes('Invalid request body')) throw new Error(`Schema mismatch: ${text}`);
throw new Error(`HTTP ${res.status}: ${text}`);
}
} catch (e) { /* log and surface schema error to caller */ } Prevention
- Generate the client payload from the SearchRagReqBody schema rather than hand-writing JSON
- Always JSON.stringify the body and set Content-Type: application/json
- Log the exact body sent and diff against the server's struct definition
- Add a client-side JSON-schema validator for endpoint payloads
When it happens
Trigger: POST to the /search_rag endpoint with a JSON body that is valid JSON but not a valid SearchRagReqBody: `{}` (missing fields), `{"name": "myrag"}` (missing input), `{"name": 123, "input": "q"}` (wrong type), or `{"name":"r","input":"q","extra":1}` when unknown fields are rejected.
Common situations: Clients sending OpenAI-style or homemade payloads instead of the endpoint's schema; forgetting that `name` refers to the rag file name registered in config; sending form-encoded or multipart data that accidentally parses as JSON but lacks fields; API version drift where the client was written against an older body schema.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09).
Data as JSON: /api/errors/714903e229b46dfd.
Report an issue: GitHub.
Appendix: source
Thrown at src/serve.rs:243
Ok(res)
}
fn list_rags(&self) -> Result<AppResponse> {
let data = json!({ "data": self.rags });
let res = Response::builder()
.header("Content-Type", "application/json; charset=utf-8")
.body(Full::new(Bytes::from(data.to_string())).boxed())?;
Ok(res)
}
async fn search_rag(&self, req: hyper::Request<Incoming>) -> Result<AppResponse> {
let req_body = req.collect().await?.to_bytes();
let req_body: Value = serde_json::from_slice(&req_body)
.map_err(|err| anyhow!("Invalid request json, {err}"))?;
debug!("search rag request: {req_body}");
let SearchRagReqBody { name, input } = serde_json::from_value(req_body)
.map_err(|err| anyhow!("Invalid request body, {err}"))?;
let config = Arc::new(RwLock::new(self.config.clone()));
let abort_signal = create_abort_signal();
let rag_path = config.read().rag_file(&name);
let rag = Rag::load(&config, &name, &rag_path)?;
let rag_result = Config::search_rag(&config, &rag, &input, abort_signal).await?;
let data = json!({ "data": rag_result });
let res = Response::builder()
.header("Content-Type", "application/json; charset=utf-8")
.body(Full::new(Bytes::from(data.to_string())).boxed())?;
Ok(res)
}
async fn chat_completions(&self, req: hyper::Request<Incoming>) -> Result<AppResponse> {View on GitHub (pinned to 82976d349a)