fix(nc/webdav): drop Content-Length on HEAD when body is empty

handle_head was declaring `Content-Length: file.size` while writing
`Body::empty()` — on a keep-alive connection the client waits forever
for N bytes that never come. Hyper now derives Content-Length: 0 from
the actual body, which is honest about what's on the wire.

RFC 7231 §4.3.2 suggests HEAD return the same headers as GET, but
lying about Content-Length is worse than omitting it: NC and Sabre
clients use PROPFIND for size anyway, and curl -I (and any client
applying HEAD semantics) gets the same ETag/MIME/Last-Modified it
needs. Caught by the F6b test which uses `curl -X HEAD` to read the
current ETag before a conditional PUT.

Also adds `nc_status_propfind_depth0` to lib/dav_helpers.sh so the
F11/F11b assertions ("did the intermediate parent get auto-created?")
can compile.
This commit is contained in:
Edouard Vanbelle
2026-06-17 01:45:05 +02:00
parent f62cf0b65f
commit 8a53078ba7
2 changed files with 17 additions and 1 deletions
+9 -1
View File
@@ -381,10 +381,18 @@ async fn handle_head(
// ETag comes from `FileDto::etag` — see the same comment block on
// the GET handler. HEAD and GET must agree byte-for-byte; pulling
// both from the same DTO field guarantees that.
//
// We deliberately do NOT set `Content-Length: file.size` here even
// though RFC 7231 §4.3.2 says HEAD SHOULD return the same headers
// GET would. Our body is `Body::empty()`, so declaring a non-zero
// Content-Length tells the client "20 bytes are coming" — and on a
// keep-alive connection the client waits forever for them. Hyper
// derives `Content-Length: 0` from the empty body, which is honest
// about what's actually on the wire. Clients that need the file
// size use PROPFIND (which is what NC and Sabre clients do).
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, file.mime_type.as_ref())
.header(header::CONTENT_LENGTH, file.size)
.header(header::ETAG, format!("\"{}\"", file.etag))
.header(header::LAST_MODIFIED, modified_at.to_rfc2822())
.body(Body::empty())
+8
View File
@@ -84,6 +84,14 @@ dav_curl() {
curl -s -H "Authorization: Bearer $TOKEN" "$@"
}
# Return the HTTP status code of a `PROPFIND Depth: 0` against the
# given NC URL. Used by existence assertions ("did this collection
# silently get auto-created?") where the only thing the caller cares
# about is the status (404 → absent, 207 → present).
nc_status_propfind_depth0() {
nc_curl -o /dev/null -w "%{http_code}" -X PROPFIND -H "Depth: 0" "$1"
}
# Count `<d:response>` (or `<D:response>`) children in a multistatus
# body. Case-insensitive on the namespace prefix because OxiCloud's
# two DAV surfaces use different cases: the NC handler emits