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
- Check the status in the message: 404 means access/sharing, 401 means token, 400 means bad id format
- For 404, share the page (or its parent database) with the integration via '...' > Connections and retry
- Verify page_id is the full 32-hex UUID from the page URL, in 8-4-4-4-12 groups or as one string
- For 401, rotate the integration secret and update the configured api_key
- 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
- Derive page ids from search results, not from URLs pasted by humans
- When a workspace reorganizes pages, re-share the new parents with the integration
- Log the page id with failures so operators can check sharing in the Notion UI
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
- Notion create_page failed ({status}): {truncated}
- Notion update_page failed ({status}): {truncated}
- Notion query_database failed ({status}): {truncated}
- Notion search failed ({status}): {truncated}
- Failed to initiate call: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/233645ae191a12d1.
Report an issue: GitHub.