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

  1. Include both required fields with correct types: `{"name": "<rag-file-name>", "input": "<query string>"}`
  2. Send `Content-Type: application/json` and a raw JSON body, not form data
  3. Check the exact field names and types of SearchRagReqBody in src/serve.rs and match your client payload to it
  4. 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

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)