Tencent/WeKnora · error

%w: %v

Error message

%w: %v

What it means

After the retry loop in doRequest exhausts all attempts without a 2xx or an immediate error return, the accumulated lastErr (rate limited / server error / transport error) is wrapped with the sentinel datasource.ErrFetchFailed using %w so callers can errors.Is() against it. If lastErr is somehow nil (loop ended without setting it), bare ErrFetchFailed is returned. This is the generic 'fetch ultimately failed' terminal error for all Notion API calls.

Source

Thrown at internal/datasource/connector/notion/client.go:149

				continue
			}

		case resp.StatusCode >= 500:
			lastErr = fmt.Errorf("server error %d: %s", resp.StatusCode, string(respBody))
			if attempt < maxRetries {
				if sErr := sleepWithContext(ctx, time.Duration(1<<attempt)*time.Second); sErr != nil {
					return nil, sErr
				}
				continue
			}

		default:
			return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))
		}
	}

	if lastErr != nil {
		return nil, fmt.Errorf("%w: %v", datasource.ErrFetchFailed, lastErr)
	}
	return nil, datasource.ErrFetchFailed
}

// Ping verifies the API token is valid by calling GET /v1/users/me.
func (c *notionClient) Ping(ctx context.Context) error {
	_, err := c.doRequest(ctx, http.MethodGet, "/v1/users/me", nil)
	return err
}

// SearchPages returns all pages and databases accessible to the integration.
func (c *notionClient) SearchPages(ctx context.Context) ([]notionPage, error) {
	return c.paginatePages(ctx, http.MethodPost, "/v1/search")
}

// GetPage retrieves a single page by ID.
func (c *notionClient) GetPage(ctx context.Context, pageID string) (*notionPage, error) {
	respBody, err := c.doRequest(ctx, http.MethodGet, "/v1/pages/"+pageID, nil)

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Unwrap with errors.Is(err, datasource.ErrFetchFailed) then inspect the wrapped cause to distinguish rate-limit vs 5xx vs network failure.
  2. Verify network egress: curl https://api.notion.com/v1/users/me from the host to confirm connectivity and DNS.
  3. Check proxy env vars (HTTPS_PROXY) are correct if the host requires egress through a proxy.
  4. If persistent 429, reduce request volume or wait for the rate budget to reset.
  5. If persistent 5xx, check status.notion.so for an incident and retry the sync later.

Example fix

// before: opaque handling
if err != nil { return err }
// after: branch on the sentinel
if errors.Is(err, datasource.ErrFetchFailed) {
    log.Printf("notion fetch failed permanently: %v", err)
    // schedule retry / mark sync failed
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: confirm the token and network before long syncs
if err := client.Ping(ctx); err != nil {
    return fmt.Errorf("notion pre-flight failed: %w", err)
}

Try / catch

if errors.Is(err, datasource.ErrFetchFailed) {
    switch {
    case strings.Contains(err.Error(), "rate limited"):
        // back off and reschedule
    case strings.Contains(err.Error(), "server error"):
        // retry later, likely Notion incident
    default:
        // network problem: check egress/proxy/DNS
    }
}

Prevention

When it happens

Trigger: Any doRequest call where all 4 attempts fail identically: persistent 429s, persistent 5xx, or persistent transport errors (connection refused, DNS failure, timeouts) from Ping, GetPage, GetDatabaseInfo, GetDataSourceInfo, GetBlockChildrenFlat, or getBlockChildrenRecursive.

Common situations: Network egress blocked from the WeKnora host to api.notion.com (firewall, VPC without NAT); DNS resolution failures; Notion outage lasting longer than the ~7s retry window; ctx cancellation during backoff sleeps returns ctx.Err() instead, so this specifically fires for sustained failures.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/62465e302fccada2. Report an issue: GitHub.