zeroclaw-labs/zeroclaw · error · anyhow::Error

Notion search failed ({status}): {truncated}

Error message

Notion search failed ({status}): {truncated}

What it means

The Notion tool's search action sent POST https://api.notion.com/v1/search and received a non-2xx status; the message carries the status and a truncated error body. Because search only touches content already shared with the integration, failures here are usually authentication, rate limiting, or malformed query payloads rather than per-object access.

Source

Thrown at crates/zeroclaw-tools/src/notion_tool.rs:174

    /// Search the Notion workspace by query string.
    async fn search(&self, query: &str) -> anyhow::Result<serde_json::Value> {
        let url = format!("{NOTION_API_BASE}/search");
        let body = json!({ "query": query });
        let resp = self
            .http
            .post(&url)
            .headers(self.headers()?)
            .json(&body)
            .timeout(std::time::Duration::from_secs(NOTION_REQUEST_TIMEOUT_SECS))
            .send()
            .await?;
        let status = resp.status();
        if !status.is_success() {
            let text = resp.text().await.unwrap_or_default();
            let truncated =
                crate::util_helpers::truncate_with_ellipsis(&text, MAX_ERROR_BODY_CHARS);
            anyhow::bail!("Notion search failed ({status}): {truncated}");
        }
        resp.json().await.map_err(Into::into)
    }
}

#[async_trait]
impl Tool for NotionTool {
    fn name(&self) -> &str {
        "notion"
    }

    fn description(&self) -> &str {
        "Interact with Notion: query databases, read/create/update pages, and search the workspace."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. For 429, throttle and retry with backoff; cache search results instead of searching per operation
  2. For 401/403, regenerate the integration secret and re-share the workspace content
  3. For 400, simplify the payload: drop filter/sort and send only the query string
  4. Remember search only sees pages/databases explicitly shared with the integration; empty results with 200 are a sharing issue, not this error

Example fix

// before (tight loop)
for page in candidates { tool.execute({"action":"search","query":page}) }
// after (search once, then act)
let found = tool.execute({"action":"search","query":"meeting notes"}).await?;
for page in found.results { tool.execute({"action":"read_page","page_id":page.id}).await? }
Defensive patterns

Strategy: retry

Validate before calling

// Throttle searches yourself: token bucket at ~2/s leaves headroom
let mut bucket = governor_default();
for q in queries { bucket.wait().await; notion.execute(json!({"action":"search","query":q})).await?; }

Try / catch

Err(e) if e.to_string().starts_with("Notion search failed (429)") => {
    tokio::time::sleep(Duration::from_secs(2u64.pow(attempt).min(32))).await; // retry
}

Prevention

When it happens

Trigger: Invalid or revoked integration token (401), token without workspace access (403), a filter/sort payload with an invalid shape or unknown property (400), or hammering the endpoint past ~3 requests/second (429 rate_limited).

Common situations: Agents that call search before every page operation quickly trip 429. Token rotation without updating config produces sudden 401s on the very first call of a session.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/99127d1c5724397f. Report an issue: GitHub.