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

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

Error message

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

What it means

The Notion tool's read_page action sent GET https://api.notion.com/v1/pages/{page_id} and got a non-2xx response. The status code and a 500-character excerpt of Notion's error body are included in the message so the underlying Notion error code is visible.

Source

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

        resp.json().await.map_err(Into::into)
    }

    /// Read a single Notion page by ID.
    async fn read_page(&self, page_id: &str) -> anyhow::Result<serde_json::Value> {
        let url = format!("{NOTION_API_BASE}/pages/{page_id}");
        let resp = self
            .http
            .get(&url)
            .headers(self.headers()?)
            .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 read_page failed ({status}): {truncated}");
        }
        resp.json().await.map_err(Into::into)
    }

    /// Create a new Notion page, optionally within a database.
    async fn create_page(
        &self,
        properties: &serde_json::Value,
        database_id: Option<&str>,
    ) -> anyhow::Result<serde_json::Value> {
        let url = format!("{NOTION_API_BASE}/pages");
        let mut body = json!({ "properties": properties });
        if let Some(db_id) = database_id {
            body["parent"] = json!({ "database_id": db_id });
        }
        let resp = self
            .http
            .post(&url)

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Check the status in the message: 404 means access/sharing, 401 means token, 400 means bad id format
  2. For 404, share the page (or its parent database) with the integration via '...' > Connections and retry
  3. Verify page_id is the full 32-hex UUID from the page URL, in 8-4-4-4-12 groups or as one string
  4. For 401, rotate the integration secret and update the configured api_key
  5. Use the search action first to obtain valid page ids instead of hand-copying them

Example fix

// before
{"action":"read_page","page_id":"My+Meeting+Notes"}
// after: resolve the real id via search, then read
{"action":"search","query":"Meeting Notes"}
{"action":"read_page","page_id":"<uuid-from-search-result>"}
Defensive patterns

Strategy: try-catch

Validate before calling

// Resolve page ids via search first instead of trusting hand-copied ids
let found = notion.execute(json!({"action":"search","query":title})).await?;
// only read ids that came back in found["results"]

Type guard

fn is_page_result(v: &serde_json::Value) -> bool {
    v.get("object").and_then(|o| o.as_str()) == Some("page")
        && v.get("id").map(|i| i.is_string()) == Some(true)
}

Try / catch

Err(e) if e.to_string().starts_with("Notion read_page failed (404)") => {
    // treat as missing/unshared: skip or re-share, never retry-loop
}

Prevention

When it happens

Trigger: Reading a page_id that was never shared with the integration (404 object_not_found), a deleted or archived page, an invalid/expired Bearer token (401), a token from a different workspace (403), or a malformed page id (400 invalid_request_error).

Common situations: Most often the page exists but the integration lacks access because only some pages in a workspace were shared. Also common: copying the page URL fragment wrong (missing a hex block of the UUID), and tokens revoked when someone reorganized integrations.

Related errors


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