From f9bf6d823e2a90a12766232fac2069216ead15cd Mon Sep 17 00:00:00 2001 From: Andrew Furman <7343697+furmandev@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:58:22 -0700 Subject: [PATCH] lib/rest: fix URLPathEscapeAll breaking WebDAV servers (eg nzbdav) with strict path matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URLPathEscapeAll was only passing [A-Za-z0-9/] through unencoded, causing it to percent-encode RFC 3986 unreserved characters (-, ., _, ~). Per RFC 3986 §2.3, unreserved characters MUST NOT be percent-encoded, and a URI that unnecessarily encodes them is not equivalent to one that does not. Servers that perform strict path matching without normalising percent-encoded characters will reject the over-encoded form with a 404. Before: /files/my-report.pdf → /files/my%2Dreport%2Epdf After: /files/my-report.pdf → /files/my-report.pdf Reserved characters (spaces, semicolons, colons, etc.) continue to be encoded as before. --- lib/rest/url.go | 7 +++++-- lib/rest/url_test.go | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/rest/url.go b/lib/rest/url.go index fcda35693..3e6b7188d 100644 --- a/lib/rest/url.go +++ b/lib/rest/url.go @@ -28,7 +28,9 @@ func URLPathEscape(in string) string { // URLPathEscapeAll escapes URL path the in string using URL escaping rules // -// It escapes every character except [A-Za-z0-9] and / +// It escapes every character except the RFC 3986 unreserved characters +// [A-Za-z0-9-._~] and the path separator /. Unreserved characters MUST NOT +// be percent-encoded per RFC 3986 §2.3. func URLPathEscapeAll(in string) string { var b strings.Builder b.Grow(len(in) * 3) // worst case: every byte escaped @@ -36,7 +38,8 @@ func URLPathEscapeAll(in string) string { for i := range len(in) { c := in[i] if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || c == '/' { + (c >= '0' && c <= '9') || c == '/' || + c == '-' || c == '.' || c == '_' || c == '~' { b.WriteByte(c) } else { b.WriteByte('%') diff --git a/lib/rest/url_test.go b/lib/rest/url_test.go index 263634879..df3519c13 100644 --- a/lib/rest/url_test.go +++ b/lib/rest/url_test.go @@ -66,7 +66,10 @@ func TestURLPathEscapeAll(t *testing.T) { want string }{ {"", ""}, - {"/hello.txt", "/hello%2Etxt"}, + // RFC 3986 unreserved characters must not be encoded + {"/hello.txt", "/hello.txt"}, + {"file-name_v2.~bak", "file-name_v2.~bak"}, + // Reserved and other characters must be encoded {"With Space", "With%20Space"}, {"With Colon:", "With%20Colon%3A"}, {"With Percent%", "With%20Percent%25"},