A WebDAV integration can list a directory, download a file, and still be unsafe to use for conditional writes.
That is not hypothetical. In WebDAVClient’s independent-server tests, one pinned server accepted a MOVE guarded by a valid lock token and a deliberately false ETag. Another accepted conditional PUTs that should have been rejected and replaced the stored bytes.
The HTTP requests completed. The application’s assumptions did not.
This is why “supports WebDAV” is too broad to be a useful compatibility statement. A better statement names the server version, backend, authentication, operation, conditions, and configuration that were actually exercised.
Here are the failure patterns that matter most when writing a .NET client or integrating a file-storage service, and what automated testing can establish about them.
1. HTTP 207 is a container for outcomes
For an ordinary HTTP request, checking the outer status is often a reasonable first step. With WebDAV, it is not always the last one.
A 207 Multi-Status response can describe multiple resources, or multiple property outcomes for one resource. This illustrative PROPFIND result contains both success and failure:
<d:multistatus xmlns:d="DAV:" xmlns:x="urn:example:metadata">
<d:response>
<d:href>/dav/report.txt</d:href>
<d:propstat>
<d:prop>
<d:getcontentlength>4096</d:getcontentlength>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<x:private-note />
</d:prop>
<d:status>HTTP/1.1 403 Forbidden</d:status>
</d:propstat>
</d:response>
</d:multistatus>
The content length is available. The private note is not. Turning that response into one boolean loses information the caller needs.
The interpretation also depends on the operation. An unavailable optional property is different from a failed member in a recursive mutation. A missing resource in a collection-sync report has profile-specific meaning; it should not be generalized to every 404 in every DAV response.
In WebDAVClient’s modern API:
EnsureHttpSuccess()checks the outer HTTP status.- Operation-specific
EnsureSucceeded()evaluates the corresponding result contract. - Callers that permit unavailable optional properties can inspect resource and property outcomes directly.
Malformed XML and interrupted parsing are not successful partial observations. A streamed listing becomes complete only after full enumeration and document validation. If a synchronization routine deletes local files merely because they were absent from an interrupted listing, the parser’s error becomes data loss.
Design rule: preserve status at the level where it applies. Do not flatten resource and property outcomes into the outer HTTP status.
2. URI encoding is resource identity
Storage tests built around test.txt miss a surprising amount of behavior.
Consider a filename containing Unicode, spaces, a literal plus, and a percent sign:
résumé + 100%.txt
The client must encode those filename bytes once, as a path segment. It must not apply HTML form rules that treat + as a space, or repeatedly decode a response href until it “looks right.”
An encoded slash and a path separator are not interchangeable. Nor is %252F the same reference as %2F. Case-sensitive path components remain case-sensitive.
For application-provided filenames, a segment-aware API such as DavPath.FromSegments(...) avoids hand-built URL concatenation. Server-provided hrefs need their own structural and identity validation before they are used as targets.
This is not an argument for rejecting every alternate spelling. RFC-aware normalization can recognize permitted equivalences, including escape-hex casing, without treating every reserved character as interchangeable.
A compatibility option should have a narrow boundary
The tested Apache filesystem configuration can return a PROPPATCH href with a literal + where the request used %2B. WebDAVClient 3.2.0 has an explicit, default-off PropPatchOptions.AllowPathPlusEncodingAlias option for verified configurations.
That is not a general URL-decoding switch. It does not equate encoded slashes, change query data, ignore path case, or relax origin checks.
This distinction matters: fixing one observed spelling difference with a global “decode everything” rule can make two different resources appear identical.
The client also checks scheme, host, and effective port for resource references, and rejects user information and fragments. Same-origin validation does not replace an application’s collection-level authorization or containment rules.
Design rule: make a compatibility exception smaller than the problem it solves, and require callers to select it explicitly.
3. A lock token and an ETag answer different questions
A lock token identifies lock state that the server must evaluate. An ETag is a representation validator. Possessing a token does not prove the content is still the version your application read.
In a DAV If expression, conditions in one list are ANDed; alternative lists are ORed. Reorganizing the parentheses can change the condition without changing any token or ETag value.
A tagged source condition can look like this, using a synthetic token:
If: <https://storage.example.com/dav/source.txt> (<urn:example:lock-1> ["known-version"])
This is different from merely submitting the token. It ties a token-and-ETag condition to a particular resource.
MOVE can also involve destination locks and other resources. A single-source example is not a general recipe for every locked source/destination combination.
The test that exposed the WsgiDAV problem
Testing a valid token was not enough. Testing only a wrong token was not enough either: another lock-enforcement path could reject the request while ignoring the tagged ETag.
The decisive test used:
- A real, confirmed source lock.
- The valid token from that lock.
- A deliberately false ETag in the same condition list.
- An absent destination.
The pinned WsgiDAV 4.3.5 / Cheroot 11.1.2 configuration returned 201 to the MOVE and created the destination when the resource tag was a full absolute URI.
The investigation found a literal-key mismatch: that server implementation retained the full resource-tag string, but looked up the resource using a quoted absolute path. The ETag’s tagged condition was not applied as intended.
Both full absolute URIs and absolute-path resource tags are allowed by RFC 4918 sections 8.3 and 10.4.2. The original client form was not malformed merely because this server mishandled it.
A verified opt-in, not automatic fallback
The merged 3.3.0 source adds DavIfConditions.TaggedAbsolutePaths(...). It selects the standards-valid path form before the request is sent:
var conditions = DavIfConditions.TaggedAbsolutePaths(
new DavTaggedConditions(source, new DavConditionList(
DavCondition.StateToken(lockToken),
DavCondition.EntityTag(strongEtag))));
var result = await client.MoveAsync(source, destination,
new MoveOptions { Conditions = conditions }, cancellationToken);
result.EnsureSucceeded();
This fragment assumes a configured client, a confirmed source lock token, a quoted ETag, and application-appropriate source/destination paths. Its condition types are in WebDAVClient.Protocol.
Availability matters: as of October 23, 2026, this API is merged but not published on NuGet. Published 3.2.0 does not contain it. Existing Tagged(...) expressions keep their absolute-URI form.
Before rendering a path-only tag, the implementation still resolves and validates each complete resource URI against the configured origin. It preserves the escaped path, query, and condition lists.
With the explicit path form, independent Windows and Linux runs returned 412 for both wrong-token and valid-token/false-ETag MOVE. The tests confirmed unchanged source bytes and ETag, and an absent destination. Matching conditions then produced 201, exact destination bytes, and an absent source.
The original default-form failure remains recorded. The library does not retry a failed MOVE using another spelling: after an ambiguous mutation, that would be a new operation against potentially changed state.
Design rule: separately negate every condition whose enforcement matters, then check the storage state, not just the response code.
4. Server rejection and unchanged data are separate assertions
A test named “conditional upload fails” can be weaker than it sounds.
If the attempted replacement uses the same bytes already stored, an ignored precondition may be invisible. If the test checks only a response code, it can miss a mutation that happened anyway.
The guarded-write tests use distinct rejected payloads. After a rejected operation, they read back the stored bytes and validator. For MOVE, they also verify that no destination appeared.
That exposed a separate restriction in the pinned nginx 1.28.0 built-in DAV profile: existing-resource If-None-Match: * and stale If-Match PUTs returned 204 and replaced the stored bytes.
A successful conditional write must mean the condition was enforced, not just that the server accepted the upload.
A preliminary HEAD is not an equivalent fix. Another writer can change the file between HEAD and PUT. Removing the condition or replaying unconditionally makes the race worse.
Weak validators require different care. The tested Apache 2.4.65 profile returned weak ETags on HEAD, so tests requiring a strong baseline could not proceed. That is a missing prerequisite for those tests, not proof that weak ETags themselves violate HTTP. The suite did not strip W/ or relabel blocked operations as passes.
Design rule: distinguish a confirmed integrity failure from an unsupported feature, a failed prerequisite, and missing evidence.
5. Publish a compatibility matrix with its conditions attached
These are the relevant suite 1.1 observations, per target framework. The runs exercised both .NET 8 and .NET 10:
| Pinned profile | Passed | Optional unsupported/unavailable | Failed / blocked |
|---|---|---|---|
| WsgiDAV 4.3.5 + Cheroot 11.1.2, default absolute-URI tags | 49 | 2 | 4 / 1 |
| Same WsgiDAV profile, explicit absolute-path tags | 54 | 2 | 0 / 0 |
| SabreDAV 4.7.0 + PHP 8.3.25, default absolute-URI tags | 55 | 1 | 0 / 0 |
| Same SabreDAV profile, explicit absolute-path tags | 55 | 1 | 0 / 0 |
| Apache 2.4.65, explicit path-plus alias configuration | 17 | 2 | 5 / 9 |
| nginx 1.28.0 built-in DAV, anonymous | 16 | 8 | 4 / 0 |
WsgiDAV and SabreDAV tag-mode comparisons used source revision 4fe7029. WsgiDAV’s explicit profile was also repeated on Linux at 1f13c83. Apache/nginx figures come from the earlier b3e2e5e guarded-mutation runs; they were not rerun for the resource-tag-only change.
These numbers are neither performance benchmarks nor a ranking of server products. They describe selected operations against pinned fixtures. For example:
- Apache’s failed setups and blocked checks reflect its weak-validator prerequisite.
- Default WsgiDAV’s accepted false-condition MOVE also causes downstream cleanup/blocked observations; the failure count is not a count of independent root causes.
- Sabre’s filesystem backend lacks positive RFC 6578 synchronization evidence even though its guarded-mutation checks pass.
- Optional unsupported/unavailable results remain limitations, not implementations of those features.
All of these source-build results identify execution revisions. They are not tests of every downloaded package version. Nextcloud, ownCloud, IIS, and untested proxy/authentication combinations do not acquire a passing status by association.
The evidence ledger retains earlier negative results. A later configured-mode pass does not rewrite what the default previously did.
6. Automate against independent implementations
A mock handler is useful for exact wire assertions, malformed-response tests, cancellation, and disposal. It is not independent-server evidence. A mock often encodes the same assumptions as the client.
The interoperability harness uses real, isolated server implementations and adds safeguards around them:
- Start a pinned fixture and check authenticated readiness.
- Prove a random run-tree path is absent, then create it and confirm ownership.
- Exercise positive and negative operations with byte/validator checks.
- Record source revision, framework, server version, authentication, and explicit compatibility options.
- Delete only owned resources and independently confirm absence.
Normal result files exclude credentials, authorization headers, lock tokens, and raw bodies. Diagnostic captures need their own narrow scope and limits; they are not a reason to dump production traffic into CI artifacts.
The suite is deliberately opt-in and mutating. Use disposable storage, not a public demo server or a valuable user directory.
After provisioning the prerequisites in the fixture guide, the current checkout can compare WsgiDAV’s modes:
.\interoperability\Run-Fixture.ps1 -Profile wsgidav
.\interoperability\Run-Fixture.ps1 -Profile wsgidav -AbsolutePathResourceTags
The first command is expected to remain nonpassing for the pinned default configuration. The second explicitly selects the path-tag mode. Both execute both target frameworks, preserve distinct result filenames, and clean up the owned fixture.
This requires the current source containing the new option, not just an installation of NuGet 3.2.0. The hosted Linux execution independently confirms the configured WsgiDAV result.
What to ask before declaring an integration ready
Replace “does this server support WebDAV?” with questions you can test:
- Can the client preserve mixed resource/property outcomes and detect an incomplete listing?
- Do filenames with Unicode, spaces, plus signs, percent signs, and reserved escapes resolve to the intended resources?
- Do stale validators and false token/ETag combinations prevent mutations?
- Do rejected operations leave bytes, validators, and namespace state unchanged?
- Are lock acquisition, refresh, release, and uncertain outcomes handled explicitly?
- Are unsupported operations, blocked prerequisites, and untested configurations visible?
This work is also how a library earns useful abstractions. A typed result should preserve distinctions callers need; a compatibility option should encode a verified exception without weakening unrelated requests.
WebDAVClient’s protocol support table, locking guide, and prioritized gaps separate implemented behavior from evidence and remaining restrictions.
Use the harness to test the operations your application depends on, then keep the results with the server configuration. A reproducible negative result is more useful than a compatibility badge that hides a lost-write risk.