fix(webdav): normalize percent-encoding case so MKCOL matches an existing PUT - #421
Conversation
… that created a resource litmus mkcol_over_plain (reusing put_get_utf8_segment's resource) sends the same UTF-8 segment with different percent-encoding hex case across the two requests. pathOf resolved paths straight from URL#pathname, which copies an already-encoded triplet through verbatim, so the two requests produced different path strings and missed each other in the backend's exact-match lookup, letting MKCOL succeed over an existing plain resource instead of 405ing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQrUELXYwiEKoRv3vnfFdX
|
The That's the Autofind runtime failing to start a Copilot session with Generated by Claude Code |
davidwkeith
left a comment
There was a problem hiding this comment.
Reviewed the diff and verified locally on claude/next-priorities-lsbmae (ba42259):
pnpm --filter @dwk/webdav build,typecheck,prettier --check,eslint, andpnpm test --project @dwk/webdav(127 tests) all pass.- The root cause and fix are correct:
URL#pathnamecopies percent-encoded triplets through verbatim without case-normalizing them, soPUT /res-%e2%82%acandMKCOL /res-%E2%82%ACresolved to different path strings even though RFC 3986 §2.1 treats them as the same octets. Uppercasing every triplet inpathOfbefore any downstream use (backend calls,authorize, lock checks,destinationOf) fixes this consistently for every verb, not justMKCOL/PUT. The new colocated test reproduces the exact litmus scenario.
One minor nit, non-blocking: normalizePercentEncoding is applied to the request pathname in pathOf, but resolved.mountPrefix/resolved.storageRoot (derived from config.mountPath/config.baseUrl in resolve()) are never normalized the same way. If a deployment ever configured a mount path containing a percent-encoded segment, the prefix-match against the now-normalized pathname could silently fail. In practice mount paths are plain-ASCII developer config, so this is very unlikely to bite — just flagging for completeness rather than asking for a change.
CONTRIBUTING.md conformance: PR title (fix(webdav): normalize percent-encoding case so MKCOL matches an existing PUT) matches Conventional Commits style with correct scope; colocated test added; changeset added (patch for @dwk/webdav); PR body keeps the Summary/Packages affected/Checklist headings verbatim from the template, and the two unchecked boxes (spec update, status.json/catalog) each carry a one-line reason rather than being silently deleted, consistent with the "leave unchecked with a reason" rule. Looks good to merge once the CI checks (still in progress as of this review) go green.
Generated by Claude Code
…path too resolve() left config.mountPath/baseUrl's percent-encoding case as given while pathOf normalized every request path, so a percent-encoded mount segment could itself drift out of sync with a differently-cased request and 404 spuriously. Apply the same normalization in resolve(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQrUELXYwiEKoRv3vnfFdX
|
Thanks for the review. Fixed the mount-path nit in 3a10cef rather than leaving it — it was cheap and closes a real (if unlikely) gap: On the Generated by Claude Code |
Summary
The 2026-07-23 hosted litmus run left
@dwk/webdav's conformance status atfailing(conformance/status.json,conformance/webdav-qa.md): thebasicgroup failed onmkcol_over_plain— anMKCOLnaming the sameUTF-8 segment
put_get_utf8_segmenthad just created as a plain resourcesucceeded instead of refusing with
405. Because the packaged litmus binarystops after the first failing group,
copymove/props/locksnever ranat all as a result.
Root cause:
pathOfresolved every request's path straight fromURL#pathname, which copies an already-percent-encoded triplet throughverbatim rather than normalizing its case.
put_get_utf8_segmentandmkcol_over_plainname the same UTF-8 segment but litmus's own requestconstruction gives the two requests different percent-encoding hex case for
it (e.g.
%e2%82%acvs%E2%82%AC) — RFC 3986 §2.1 says these are the sameoctets, but the backend's exact-string-match
stat()lookup inmkcol()didn't treat them that way, so it missed the existing resource and let the
MKCOLthrough.Fix:
pathOfnow uppercases every percent-encoded triplet before theresolved path is used anywhere downstream (backend calls, authorization,
lock/precondition checks), so encoding-case no longer affects resource
identity.
This is not yet re-verified against the hosted target —
status.jsonand the QA doc's Result table intentionally stay at
failing/pendinguntil a fresh litmus dispatch confirms
basicpasses and letscopymove/props/locksrun for the first time.conformance/webdav-qa.mdhas a new "Follow-up" section documenting the fix and that next step.
Packages affected
@dwk/webdav
Checklist
spec/packages/and updated them ifbehaviour changed — not applicable, no behaviour change to the
documented contract, just a conformance bug fix
src/*.test.ts)pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm testpnpm changeset) if this touches a publishablepackage
catalog.json/conformance/status.jsonif this adds a newmountable worker or changes conformance status — not applicable yet;
status.jsonstaysfailing/pendinguntil a fresh hosted litmusrun confirms the fix (see
conformance/webdav-qa.md's new Follow-upsection)
Generated by Claude Code