Skip to content

P2P server backups are never uploaded and use a server-invalid draft code #6728

Description

@PBernaerts

Summary

The P2P draft server-backup path cannot create a backup from the production client. There are two independent blockers:

  1. The only production new P2PDraftHost(...) call does not pass backupEndpoint, so this.backupEndpoint remains null and the upload branch is never entered.
  2. If an endpoint is supplied, the client sends the internal draft identifier (draft- plus eight lowercase hex digits), while phase-server accepts only exactly six uppercase alphanumeric characters. The request therefore returns 400 Invalid draft code.

The client does not inspect response.ok, so the second failure is silent.

Build/version

main at 9da919f61039068cfe50fb219207f36e81dd57e5

Actual behavior

P2P draft state is persisted to local IndexedDB, but the server-backup upload is not attempted by the production construction path. If backupEndpoint is injected, the server rejects the generated draft-xxxxxxxx identifier and no SQLite backup row is created.

The related open PR #5370 verifies only that fetch is called. Its test explicitly expects draft-xxxxxxxx, so it does not exercise the server validator or prove that a row was stored.

Expected behavior

When server backup is configured, a real production P2P draft should periodically create or update a backup row, surface non-2xx failures, and delete the same row on clean completion.

Code-path reproduction

  1. Create a P2P draft through DraftPodHostAdapter.initialize().
  2. Follow the sole production constructor call in client/src/adapter/draftPodHostAdapter.ts: it supplies roomCode but no backupEndpoint.
  3. P2PDraftHost.persistSession() gates uploads on this.backupEndpoint, which remains null.
  4. If an endpoint is supplied manually, start a draft. startDraft() assigns an identifier shaped as draft-xxxxxxxx and uploadBackupSnapshot() sends it as draft_code.
  5. crates/phase-server/src/admin.rs::is_valid_draft_code() rejects it because it is not six uppercase alphanumeric characters.
  6. uploadBackupSnapshot() treats an HTTP 400 as success because it awaits fetch without checking the returned status.

Relevant locations

  • client/src/adapter/draftPodHostAdapter.ts:210-223 — production constructor omits backupEndpoint
  • client/src/adapter/p2p-draft-host.ts:181-200 — omitted endpoint becomes null
  • client/src/adapter/p2p-draft-host.ts:462-496 — generated draft-xxxxxxxx identifier
  • client/src/adapter/p2p-draft-host.ts:1385-1415 — upload gate, request, and ignored HTTP status
  • crates/phase-server/src/admin.rs:16-22 — incompatible six-character validator

Impact

The advertised best-effort server backup for P2P drafts is currently inert, and future work such as #5370 can pass frontend CI without producing a usable server backup. Local IndexedDB persistence is unaffected.

Suggested acceptance criteria

  • The production construction path supplies a configured backup endpoint when server backup is available.
  • Client and server use one canonical backup identifier with a shared validation contract.
  • Upload and cleanup treat non-2xx responses as failures and surface a useful diagnostic.
  • An integration test exercises the actual HTTP handler and verifies that a real client-shaped snapshot creates, updates, retrieves, and deletes the intended SQLite row.
  • The fix: seed first P2P backup immediately to reduce row-squatting risk #5370 regression test uses an identifier accepted by that contract or is replaced by the integration test above.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions