santifer/career-ops · error · Error
Notion ->
Error message
Notion ${method} ${path} -> ${j.code}: ${j.message} What it means
The Notion client's internal api() helper calls the Notion REST endpoint and, when the HTTP response is not ok, throws an Error embedding the method, path, Notion error code (j.code, e.g. object_not_found, validation_error, rate_limited, unauthorized) and the human-readable message returned by the API. It is a thin wrapper: the root cause is always whatever Notion rejected, surfaced with request context so the developer can see which call failed.
Solutions
- Read j.code in the message: 'object_not_found' → share the page/database with your integration in Notion and verify the parent/data_source ids.
- 'unauthorized' or 'unauthorized_error' → regenerate the integration token and update NOTION_ACCESS_TOKEN in .env.
- 'validation_error' → compare the properties payload against the database's actual schema (names, types, required fields) via the API or Notion UI.
- 'rate_limited' → increase backoff/retry with exponential delay; the built-in 360ms throttle may be insufficient when other processes share the integration.
- For 5xx codes, retry after a delay — the failure is on Notion's side.
Example fix
// before — no differentiation
try { await client.createPage(dsId, props); } catch (e) { console.error(e.message); }
// after — act on the Notion error code embedded in the message
try {
await client.createPage(dsId, props);
} catch (e) {
if (/rate_limited/.test(e.message)) {
await new Promise(r => setTimeout(r, 5000)); // retry with backoff
} else if (/object_not_found/.test(e.message)) {
throw new Error('Share the target page with your Notion integration (Connections).');
} else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight: verify the token works and the parent is reachable before real writes
const probe = await fetch('https://api.notion.com/v1/users/me', { headers: { Authorization: `Bearer ${token}`, 'Notion-Version': '2025-09-03' } });
if (!probe.ok) throw new Error(`Notion token check failed: ${probe.status}`); Try / catch
try {
await client.createPage(dsId, props);
} catch (e) {
const m = /Notion (\w+) (\S+) -> ([^:]+): (.*)/.exec(e.message);
if (m) {
const [, method, path, code, msg] = m;
if (code === 'rate_limited') return retryWithBackoff();
if (code === 'object_not_found') throw new Error(`Notion object missing or not shared with integration: ${path}`);
if (code === 'unauthorized') throw new Error('Regenerate the integration token in NOTION_ACCESS_TOKEN');
throw new Error(`Notion ${code} on ${method} ${path}: ${msg}`);
}
throw e;
} Prevention
- Share every target page/database with the integration — object_not_found is the most common cause.
- Parse j.code out of the error message and branch on known codes (rate_limited, object_not_found, validation_error, unauthorized).
- Add exponential backoff for 429/5xx instead of failing the whole batch.
- Validate page property payloads against the live database schema before writing.
- Probe with GET /users/me at startup to catch auth problems before doing real work.
When it happens
Trigger: Any api() call whose response has r.ok === false: expired/insufficient integration token (401 unauthorized), page or data_source id not shared with the integration (404 object_not_found), malformed properties against the schema (400 validation_error), exceeding ~3 req/s server-side limits (429 rate_limited), or Notion 5xx outages.
Common situations: Integration not connected to the parent page → object_not_found on blocks/{id}/children; property name/type mismatches after editing the database schema → validation_error on pages POST; hammering the API in a batch loop beyond the rate limit despite the built-in 360ms sleep → rate_limited; using a token from a deleted integration → unauthorized.
Related errors
- API error: code=
- Apify did not return a run id
- gmail: failed to fetch message
- HTTP
- nofluffjobs: unexpected API response — expected
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/76109958c8514ce8.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/notion/_notion.mjs:77
/**
* Build a Notion client bound to one user's token + parent page. Network goes
* through the injected `fetchFn` (the plugin passes ctx.fetch so the engine's
* allowedHosts/HTTPS/redirect guard applies); falls back to global fetch for
* standalone use. Nothing here reads process.env.
* @param {{ token: string, parent: string, fetch?: Function }} cfg
*/
export function createNotionClient({ token, parent, fetch: fetchFn = globalThis.fetch }) {
if (!token) throw new Error('NOTION_ACCESS_TOKEN is not set (.env) — the Notion plugin needs it to read/write.');
const HEADERS = { Authorization: `Bearer ${token}`, 'Notion-Version': '2025-09-03', 'Content-Type': 'application/json' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function api(path, method, body) {
await sleep(360); // ~3 req/s
// ctx.fetch throws on non-2xx (its message carries the body); the !r.ok
// branch below is the fallback when a plain global fetch is injected.
const r = await fetchFn(`https://api.notion.com/v1/${path}`, { method, headers: HEADERS, body: body ? JSON.stringify(body) : undefined });
const j = await r.json();
if (!r.ok) throw new Error(`Notion ${method} ${path} -> ${j.code}: ${j.message}`);
return j;
}
/** Create a page in a data source. `markdown` (optional) becomes the page body. */
async function createPage(dataSourceId, properties, markdown) {
const body = { parent: { type: 'data_source_id', data_source_id: dataSourceId }, properties };
if (markdown) body.markdown = markdown;
return api('pages', 'POST', body);
}
/** Map of DB name → primary data source id for every DB under the parent page. */
async function resolveDBs() {
if (!parent) throw new Error('Set NOTION_PARENT_PAGE_ID in .env (the "Career Ops" parent page id).');
const out = {};
let cursor;
do {
const j = await api(`blocks/${parent}/children?page_size=100${cursor ? `&start_cursor=${cursor}` : ''}`, 'GET');
for (const b of j.results) {View on GitHub (pinned to aac998c7ed)