Skip to content

Fix WebDAV sync: handle 409 on missing parents, improve slug stability#53

Merged
TheZupZup merged 1 commit into
mainfrom
claude/fix-webdav-sync-errors-pp3Bi
May 3, 2026
Merged

Fix WebDAV sync: handle 409 on missing parents, improve slug stability#53
TheZupZup merged 1 commit into
mainfrom
claude/fix-webdav-sync-errors-pp3Bi

Conversation

@TheZupZup
Copy link
Copy Markdown
Owner

Summary

This PR fixes critical WebDAV sync issues where clients received 409 Conflict errors when pushing notes to newly-created notebooks, and improves slug stability across MKCOL → PUT sequences. It also hardens error handling to prevent 500 Internal Server errors from leaking stack traces.

Key Changes

WebDAV Provider Improvements (webdav_provider.py)

  • Slug system overhaul: Introduced _parse_slug(), _notebook_slug(), and _id_with_prefix() helpers to ensure slugs remain stable when clients and servers both generate UUIDs. Slugs now follow the format <title-slug>__<id-prefix> where the 8-char hex prefix disambiguates items with identical titles.
  • MKCOL support: Added create_collection() methods to RootCollection and NotebookCollection to allow clients to create notebooks and notes via WebDAV MKCOL requests. These methods parse the slug to preserve the client's id-prefix, ensuring the path remains valid for subsequent PUT operations.
  • Auto-create empty resources: Implemented create_empty_resource() on NoteCollection to auto-allocate pages when clients PUT to page_N.ink files that don't yet exist, eliminating 409 errors from missing parent resources.
  • Slug lookup resilience: Added _find_notebook_by_slug() and _find_note_by_slug() helpers that resolve slugs via multiple strategies (canonical slug, id-prefix match, slugified name) to handle renames and legacy paths gracefully.
  • Error handling: Introduced _safe_dav_error() to wrap non-DAVError exceptions into proper 500 DAVError responses with readable messages instead of leaking stack traces. Fixed ETag generation with _safe_etag() to avoid quote-escaping issues that crashed WsgiDAV on PUT to existing notes.
  • HTTP status code compatibility: Made HTTP error code imports forward-compatible using getattr() to handle different WsgiDAV versions.

Sync Client Improvements (client.py)

  • 409 recovery with MKCOL retry: Modified put_note_meta() and put_ink_page() to transparently MKCOL missing parent collections when the server returns 409, then retry the PUT once. This makes push reliable even when the server hasn't seen a notebook/note yet.
  • Unified PUT helper: Extracted _put_json() internal method to centralize PUT logic with consistent error reporting and MKCOL-on-409 recovery for both note metadata and ink pages.
  • Better error messages: Added _format_http_failure() to surface backend error details (capped at 200 chars) for 5xx responses so users see the actual problem instead of an opaque status code.

Server Bootstrap (server.py)

  • Storage layout guarantee: Added ensure_storage_layout() to defensively recreate WebDAV directories (notes/, drawings/, notebooks/) if they're wiped at runtime.
  • Default notebook: Added ensure_default_notebook() to create the fallback "Uncategorized" notebook (with id prefix 00000000) on startup, ensuring the parent collection exists before clients' first PUT.

Comprehensive Test Suite (test_webdav_sync_push.py)

  • Live WebDAV server fixture that spins up a real WsgiDAV app on a random port for end-to-end testing.
  • Unit tests for slug parsing, id-prefix preservation, and MKCOL idempotency.
  • Integration tests verifying MKCOL → PUT chains work without 409 errors.
  • Tests confirming 409 (not 500) is returned for direct PUT to missing parents, with readable error bodies.
  • End-to-end sync engine tests covering happy-path push, 409 recovery, and uncategorized notebook fallback.

Notable Implementation Details

  • Slug round-tripping: The id-prefix in slugs (e.g., my-note__abcd1234) is extracted and used to mint UUIDs that start with those 8 hex chars, so MKCOL → PUT sequences don't

https://claude.ai/code/session_011hfnfHGLVJR1aUerZ5sZD6

Push was failing on the new file-based backend with 409 Conflicts on note
metadata/page uploads and a 500 Internal Server Error on the welcome
note. Root causes:

  - The WebDAV provider had no MKCOL support on the root collection, so
    notebook directories never persisted server-side. Subsequent PUTs to
    nested paths returned 409 (parent missing).
  - NotebookCollection.create_collection minted a fresh note id, so the
    slug returned to the caller no longer matched the slug the client
    requested — every follow-up PUT 409'd against the lost slug.
  - NoteCollection had no create_empty_resource, so PUT to a not-yet-
    existing page_N.ink (e.g. a new page during sync) bubbled up as a
    permission error.
  - NoteMetaFile.get_etag emitted a quoted token, which WsgiDAV's
    checked_etag rejects — every PUT to an existing note (the welcome
    note in particular) crashed with 500.
  - Writer crashes (DB errors, malformed JSON) propagated as bare 500
    responses instead of typed DAVErrors.

Fix:

  - RootCollection.create_collection materialises a notebook with an id
    whose first 8 hex chars match the slug, so MKCOL → PUT round-trips
    cleanly. NotebookCollection does the same for notes.
  - NoteCollection.create_empty_resource provisions new pages on PUT.
  - _NoteMetaWriter migrates the placeholder id to the client's id when
    the PUT body declares a different one.
  - get_etag now returns a quote-free token; WsgiDAV adds the quotes.
  - Writers convert exceptions into DAVError with a useful context_info,
    so callers get 400/500 with reasons instead of opaque "500 internal".
  - The server bootstraps the "Uncategorized" fallback notebook so the
    very first push from a fresh client has a valid parent collection.
  - The sync client MKCOLs missing parents on a 409 and retries the PUT
    once. Failure messages now include 5xx body snippets.

Tests:

  - tests/test_webdav_sync_push.py covers slug parsing, MKCOL idempotency,
    PUT into missing parents (live WsgiDAV server), end-to-end sync push,
    server bootstrap, and writer error reporting.

https://claude.ai/code/session_011hfnfHGLVJR1aUerZ5sZD6
@TheZupZup TheZupZup merged commit 6e8604d into main May 3, 2026
1 check passed
@TheZupZup TheZupZup deleted the claude/fix-webdav-sync-errors-pp3Bi branch May 3, 2026 07:34
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72eb1b4a0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# du UUID, pour différencier des notes au même titre.

_SLUG_SEPARATOR = "__"
_HEX_RE = re.compile(r"^[0-9a-f]{1,16}$")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject slug id-prefixes longer than 8 hex chars

_parse_slug() accepts up to 16 hex chars, but _id_with_prefix() only preserves the first 8 when creating the UUID, so paths like nb__123456789 are created successfully and then immediately become unreachable at the same slug (get_resource_inst('/nb__123456789', ...) returns None). This breaks the MKCOL → PUT flow for any client that sends 9–16 hex chars in the suffix, because subsequent lookups still require the full parsed prefix.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants