Skip to content

Commit 2e4d1eb

Browse files
webdav: advertise DAV capability on the unauthenticated OPTIONS probe
Windows' WebClient (Mini-Redirector) sends an unauthenticated OPTIONS first and, on a bare 401, can fail to recognise the server as WebDAV — surfacing as 0x80070043 'network name not found' and the well-known intermittent-connect behaviour. Attach DAV/MS-Author-Via/Allow headers to that challenge so the very first probe identifies the server as WebDAV, while still requiring a token before any data is returned. Also expand the Windows troubleshooting docs (UNC-vs-@ssl, WebClient service restart/negative cache, BasicAuthLevel, 50 MB FileSizeLimit).
1 parent 8b14d88 commit 2e4d1eb

3 files changed

Lines changed: 53 additions & 17 deletions

File tree

apps/api/src/routes/webdav.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,28 @@ export const webdavRoutes: FastifyPluginAsync = async (app) => {
158158
}
159159

160160
async function handle(req: FastifyRequest, reply: FastifyReply) {
161-
const principal = await auth(req, reply);
162-
if (!principal) return;
163-
const ownerId = principal.ownerId;
164-
const segments = pathSegments((req.params as Record<string, string>)['*']);
165161
const method = req.method;
166162

163+
// Advertise WebDAV capability on EVERY OPTIONS — including the unauthenticated probe Windows'
164+
// Mini-Redirector sends first. Setting DAV/MS-Author-Via/Allow before the auth challenge means
165+
// Windows recognises the server as WebDAV on that first request (otherwise it only learns this
166+
// on a second, authenticated round-trip, which is a common source of "this isn't a WebDAV
167+
// site" / 0x80070043 flakiness). No user data is exposed — these are static capabilities.
167168
if (method === 'OPTIONS') {
168-
return reply
169+
reply
169170
.header('DAV', '1, 2')
170171
.header('MS-Author-Via', 'DAV')
171-
.header('Allow', 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, MOVE, COPY, LOCK, UNLOCK')
172-
.code(204)
173-
.send();
172+
.header('Allow', 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, MOVE, COPY, LOCK, UNLOCK');
173+
const principal = await auth(req, reply);
174+
if (!principal) return; // 401 challenge, now carrying the DAV headers above
175+
return reply.code(204).send();
174176
}
175177

178+
const principal = await auth(req, reply);
179+
if (!principal) return;
180+
const ownerId = principal.ownerId;
181+
const segments = pathSegments((req.params as Record<string, string>)['*']);
182+
176183
const node = await resolvePath(ownerId, segments);
177184

178185
const base = davBase(req);

apps/api/test/webdav.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ describe('webdav routing (boot smoke-test)', () => {
4949

5050
// No auth header → challenge BEFORE any DB access. Proves the custom methods are routed and
5151
// that WebDAV sits outside the CORS scope (otherwise OPTIONS would be 400/204 from cors).
52-
it('challenges an unauthenticated OPTIONS /dav', async () => {
52+
// The 401 still carries the DAV capability headers so Windows' Mini-Redirector detects WebDAV
53+
// on its first, unauthenticated probe.
54+
it('challenges an unauthenticated OPTIONS /dav but advertises DAV capability', async () => {
5355
const res = await app.inject({ method: 'OPTIONS', url: '/dav' });
5456
expect(res.statusCode).toBe(401);
5557
expect(String(res.headers['www-authenticate'])).toContain('Basic');
58+
expect(String(res.headers['dav'])).toContain('1');
59+
expect(String(res.headers['ms-author-via'])).toBe('DAV');
5660
});
5761

5862
it('routes the custom PROPFIND verb', async () => {

docs/API.md

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,37 @@ curl -u "me:ocl_YOUR_TOKEN" -X PROPFIND -H "Depth: 1" https://<host>/api/dav/
167167

168168
### Windows Explorer
169169

170-
Windows' built-in WebDAV client is strict:
171-
172-
- Use **HTTPS** (you do) and make sure the **WebClient** service is running (`services.msc`).
173-
- Windows often refuses Basic auth even over HTTPS until you set, in
174-
`HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters`, the DWORD
175-
**`BasicAuthLevel = 2`**, then restart the WebClient service.
176-
- If it still won't map, test with **rclone**, **Cyberduck** or **WinSCP** first — if those work,
177-
it's a Windows-client limitation, not the server.
170+
Windows' built-in WebDAV client (the *WebClient* / Mini-Redirector service) is strict and its
171+
errors are misleading — **`0x80070043` "The network name cannot be found"** almost always means
172+
Windows never completed the WebDAV handshake, **not** that the server is down. Work through these
173+
in order:
174+
175+
1. **Never use a `\\host\…` UNC path for an internet server.** `\\copper.forgenet.fr\api\dav\`
176+
makes Windows try **SMB (port 445)** first, which is blocked over the internet → `0x80070043`.
177+
Either:
178+
- **Map network drive** with the plain URL `https://copper.forgenet.fr/api/dav/`, **or**
179+
- if you must use a UNC path, use the SSL WebDAV form:
180+
`\\copper.forgenet.fr@SSL\api\dav\` (add `@443` after `@SSL` if you run on a non-standard port).
181+
2. **Make sure the WebClient service is running and restart it.** It caches a "this server isn't
182+
WebDAV" verdict after any failed attempt — which is exactly why it works *sometimes*. Clearing
183+
that cache fixes most intermittent failures. In an **admin** PowerShell / CMD:
184+
```
185+
net stop webclient & net start webclient
186+
```
187+
(Set its startup type to *Automatic* in `services.msc` so it's always up.)
188+
3. **Allow Basic auth over HTTPS.** Windows refuses Basic even over TLS until you set, in
189+
`HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters`, the DWORD
190+
**`BasicAuthLevel = 2`**, then restart WebClient (step 2).
191+
4. **Raise the download size cap.** Windows refuses files larger than **50 MB** over WebDAV by
192+
default. In the same `Parameters` key set the DWORD **`FileSizeLimitInBytes`** to e.g.
193+
`4294967295` (≈4 GB, the max) and restart WebClient.
194+
5. **Credentials:** leave the username as anything (it's ignored) and paste your **`ocl_…` API
195+
token as the password**. Use an *unrestricted* token — folder-scoped tokens are refused.
196+
6. If it still won't map, prove it's Windows and not the server by testing with **rclone**,
197+
**Cyberduck** or **WinSCP** — if those connect (they will), it's purely a Windows-client quirk.
198+
You can also confirm the server from any machine:
199+
```bash
200+
curl -u "me:ocl_YOUR_TOKEN" -X PROPFIND -H "Depth: 1" https://copper.forgenet.fr/api/dav/
201+
```
202+
A `207 Multi-Status` with XML = the server and proxy are perfect; the ball is in Windows' court.
178203
</content>

0 commit comments

Comments
 (0)