Fix WebDAV sync: handle 409 on missing parents, improve slug stability#53
Conversation
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
There was a problem hiding this comment.
💡 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}$") |
There was a problem hiding this comment.
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 👍 / 👎.
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)_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.create_collection()methods toRootCollectionandNotebookCollectionto 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.create_empty_resource()onNoteCollectionto auto-allocate pages when clients PUT topage_N.inkfiles that don't yet exist, eliminating 409 errors from missing parent resources._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._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.getattr()to handle different WsgiDAV versions.Sync Client Improvements (
client.py)put_note_meta()andput_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._put_json()internal method to centralize PUT logic with consistent error reporting and MKCOL-on-409 recovery for both note metadata and ink pages._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)ensure_storage_layout()to defensively recreate WebDAV directories (notes/, drawings/, notebooks/) if they're wiped at runtime.ensure_default_notebook()to create the fallback "Uncategorized" notebook (with id prefix00000000) on startup, ensuring the parent collection exists before clients' first PUT.Comprehensive Test Suite (
test_webdav_sync_push.py)Notable Implementation Details
my-note__abcd1234) is extracted and used to mint UUIDs that start with those 8 hex chars, so MKCOL → PUT sequences don'thttps://claude.ai/code/session_011hfnfHGLVJR1aUerZ5sZD6