Skip to content

Troubleshooting

FAQ format: each entry is problem → cause → fix. For the full error-semantics reference (status codes, terminal states, retry discipline) see Responses and errors and Rate limiting and errors.

Bare requests to the API get a Cloudflare 403

Problem: a hand-rolled curl / script against www.perplexity.ai REST endpoints returns 403 with a Cloudflare challenge page — even with cookies copied from the browser — while the same endpoints work through the tool.

Cause: Cloudflare sits in front of the site, and cf_clearance / __cf_bm are bound to the browser's TLS fingerprint. A bare client's fingerprint does not match, so the challenge fires. The tool passes because it uses Python urllib with cookies imported from the browser and a desktop-Chrome User-Agent (pplx_export/core/http/cookie_transport.py:29). Cloudflare can also 403 under rate control — in that case the response carries the same challenge shape.

Fix:

  • Do not bypass the tool's transport; run your call through pplx-export / pplx-ask instead of ad-hoc scripts.
  • Inside the tool, a 200 response with a non-JSON body (the Cloudflare interstitial) is classified as a transport error, not data (pplx_export/core/http/cookie_transport.py:133).
  • If 403s start appearing inside the tool, slow down (see Rate limiting) and refresh the cookies; a persistent challenge means re-login in the browser.
  • Mind the two faces of 403: a Cloudflare risk-control challenge (clears once you slow down) versus an API-level 403 (dead cookie — raised immediately with no backoff; see the next section). The design page maps the latter (rate-limiting-errors.md).

Background: API authentication.

401 errors / expired cookies

Problem: commands fail with an auth error — AuthTransportError: 鉴权失败 401 from pplx-export, or pplx-ask ask exiting with an HTTP 401/403 hint to update the cookie.

Cause: the session cookie has expired or been invalidated. 401/403 are treated as authentication failures and raised immediately — no backoff, because backoff cannot self-heal a dead session (pplx_export/core/http/cookie_transport.py:82; pplx_export/core/errors.py:68). batch additionally fail-fasts after 3 consecutive auth failures so a dead cookie does not burn through the queue.

Fix:

  1. Re-login (or re-open the site) in the browser so the session cookies are renewed.
  2. Refresh the tool's cookie cache. The cache at <out>/index/.cookies.json is reused within a 12-hour freshness window (pplx_export/core/cookies.py:39), so after re-login either:
  3. run once with --cookies-from <browser> to force a fresh browser import, or
  4. delete <out>/index/.cookies.json and let the next run re-import automatically.
  5. Every run that validates successfully re-saves the cache (pplx_export/commands/common.py:150), so day-to-day runs stay fresh on their own.

Setup details: Getting started · Configuration.

An export ran under the wrong account (multi-account)

Problem: archived threads were fetched with the wrong account's session — e.g. an --account alice run pulled data as bob, or the archive shows threads that do not belong to the intended account.

Cause: with several accounts logged into the same browser, the active session token (__Secure-next-auth.session-token) may belong to a different account than the one you targeted. If the target account's email is not registered in the user-level config, the tool cannot detect this and only logs a warning.

How the tool prevents it (pplx_export/commands/common.py:93): on startup the transport calls GET /api/auth/session and compares the live email with the registered one. On mismatch it automatically enumerates the browser's per-account session cookies (__Secure-pplx.session.<user_id>), substitutes each into the active token, and probes the session until the target email matches (pplx_export/commands/common.py:190; pplx_export/core/cookies.py:97). If no token matches, the command aborts with a clear error — it never silently proceeds as the wrong account.

Fix:

  • Register every account's email under [accounts.<name>] (see Configuration) and pass --account explicitly.
  • Check the startup log line [auth] cookie 来源 …,当前账户: … — it names the live session email before anything is fetched.
  • To audit an existing archive, each thread's thread.json carries an export_via field recording which account performed the export (pplx_export/sites/perplexity/fs_writer.py:229). pplx-export sync-deleted uses the same field to pick the account for online verification.

Mechanism depth: API authentication · Ask and accounts.

"Config file not found" — degraded mode

Problem: a startup warning says no user-level config file was found and the command runs in degraded mode; or an explicit --account alice fails with an error pointing at config.example.toml.

Cause: no config file at any of the three lookup locations — --config PATH, the PPLX_EXPORT_CONFIG environment variable, or the default ~/.config/pplx-export/config.toml (pplx_export/config.py:113). Two related but distinct cases: an explicitly specified config path that does not exist raises ConfigError; a corrupt (unparseable) config always raises ConfigError — a broken config never silently degrades.

Effects of degraded mode:

  • The account registry is empty, so cookie-ownership verification is skipped with a warning and commands run as the placeholder account default (pplx_export/commands/common.py:51). An explicit --account errors out instead.
  • pplx-ask ask skips the auto-move into the BOT space (moved_to_bot stays false in the result JSON) and telemetry carries an empty user id; asking and archiving otherwise work.
  • Archives land under the fallback account folder derived from the username.

Fix: copy config.example.toml to ~/.config/pplx-export/config.toml, fill in [accounts.<name>] (display_name / email / user_id), [bot_space], and default_account — see Configuration.

ENTRY_EXPIRED vs ENTRY_DELETED

Problem: exporting or re-syncing a thread reports ENTRY_EXPIRED or ENTRY_DELETED, and the thread can never be fetched again.

Cause: both arrive as HTTP 400 from GET /rest/thread/<uuid> with different error codes, and both are terminal — the thread no longer exists on the platform:

Code Meaning Tool mapping Terminal state
ENTRY_EXPIRED The platform purged the thread (~3-month retention) EntryExpiredError (pplx_export/core/errors.py:24) expired
ENTRY_DELETED The thread was actively deleted by the user / remote side (the downstream effect of DELETE /rest/thread/delete_thread_by_entry_uuid) EntryDeletedError, a subclass of EntryExpiredError (pplx_export/core/errors.py:30) deleted

What it means for your archive:

  • Neither state is ever retried — not by incremental sync, not with --force. The terminal mark lives in <out>/index/batch_state.json.
  • Your local archive is never deleted or moved by the tool — the repository copy is the backup. The export command registers the terminal state and exits gracefully (pplx_export/commands/export_cmd.py:51).
  • Because the subclass relationship is deliberate, code paths that only know EntryExpiredError still treat ENTRY_DELETED as terminal; aware paths (batch / export / sync-deleted / search-mode-backfill) classify it precisely as deleted.
  • Practical takeaway: export timely. Past the ~3-month purge, artifact/report source links also expire irrecoverably.

Related: Incremental sync · Responses and errors.

Assets that cannot be downloaded (toolu_ handles)

Problem: some entries in assets/assets_manifest.json have versions flagged "no_download_channel": true, and no corresponding file exists under assets/files/.

Cause: toolu_-prefixed cloud-workspace handles (DOC_FILE / CODE_FILE / UNKNOWN without a URL form) have no API download channel: GET /rest/assets/<asset_uuid>/data returns 404 ASSET_NOT_FOUND for them, and file-repository/download rejects file:repo/... handles (400). This is a known archive-completeness boundary, not a bug in the export. pplx-export assets-backfill marks these versions no_download_channel and skips them (pplx_export/commands/assets_backfill_cmd.py:356).

Fix:

  • Nothing to download today — the flag is the deliberate record of the boundary.
  • The content often survives inline: subagent page-extraction text and step payloads are preserved in the thread's raw JSON (raw_entries.json / raw_blocks.json) and in the rendered turns/ — check there first.
  • file-repository/list-files is tracked as a potential future rescue path; see API discovery roadmap.

Manifest layout: Archive layout.

Where are the logs?

Console: INFO-level progress by default; -v / --verbose switches to DEBUG (request tracing, internal decisions); warnings and errors are always shown.

File: pass --log-file to capture the full DEBUG stream (pplx_export/core/logging.py:45):

  • --log-file without a value lands at <out>/index/logs/<cmd>-<timestamp>.log (pplx_export/commands/common.py:218) — e.g. pplx-ask-ask-20260723-120000.log.
  • --log-file PATH writes to the given path.

Other state files useful for diagnosis (under <out>/index/):

File Content
.cookies.json Cookie cache (12 h freshness; written atomically with 0o600 — it is a login-equivalent credential, keep it private)
batch_state.json Per-thread export state, incl. the terminal expired / deleted marks
answer_variants_log.jsonl Answer-rewrite variant registry
library_*.json Per-account library index snapshots

See also