From 32be6998628ad0ee3b4b49dfeb81deaf85379364 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Thu, 27 Aug 2026 10:24:22 -0400 Subject: [PATCH] Fix WebDAV downloads with non-ASCII characters in the password HTTP.download() built the Basic auth header with btoa(), which throws on code points above 255, so every file download failed immediately with a TypeError. Requests that go through XMLHttpRequest were unaffected, since Necko builds the header itself, UTF-8 encoded. https://forums.zotero.org/discussion/133454/synchronization-error-after-upgrading-to-10-0-1 (cherry picked from commit 3b93d33b359a752a7b5e5e3d8c3711a77167386b) --- chrome/content/zotero/xpcom/http.js | 4 +++- test/tests/httpTest.js | 29 ++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index 712394eb0f..0c1b2b9dd8 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -694,7 +694,9 @@ Zotero.HTTP = new function () { // Build headers let headers = new Zotero.HTTP.CasePreservingHeaders(options.headers || {}); if (ctx.username) { - let encoded = btoa(ctx.username + ':' + (ctx.password || '')); + // Encode as UTF-8 before base64, since btoa() only accepts code points below 256 + let bytes = new TextEncoder().encode(ctx.username + ':' + (ctx.password || '')); + let encoded = btoa(String.fromCharCode(...bytes)); headers.set('Authorization', `Basic ${encoded}`); } fetchOptions.headers = headers; diff --git a/test/tests/httpTest.js b/test/tests/httpTest.js index 892b9bd4e1..4986a93415 100644 --- a/test/tests/httpTest.js +++ b/test/tests/httpTest.js @@ -454,15 +454,16 @@ describe("Zotero.HTTP", function () { '/download/auth', { handle: function (request, response) { - let val; - try { - val = request.getHeader("Authorization"); - } - catch (e) { - val = ""; + if (!request.hasHeader("Authorization")) { + response.setStatusLine(null, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="Test"', false); + response.write("denied"); + return; } response.setStatusLine(null, 200, "OK"); - response.setHeader("X-Echo-Auth", val, false); + response.setHeader( + "X-Echo-Auth", request.getHeader("Authorization"), false + ); response.write("ok"); } } @@ -610,6 +611,20 @@ describe("Zotero.HTTP", function () { assert.equal(req.status, 200); assert.equal(req.headers.get("X-Echo-Auth"), expected); }); + + it("should encode non-ASCII credentials the same way as request()", async function () { + let url = `http://user:p%C3%A4ssw%E2%82%ACrd` + + `@127.0.0.1:${port}/download/auth`; + let dest = PathUtils.join(tmpDir, "auth-utf8.bin"); + let req = await Zotero.HTTP.download(Services.io.newURI(url), dest); + assert.equal(req.status, 200); + + let xmlhttp = await Zotero.HTTP.request("GET", url); + assert.equal( + req.headers.get("X-Echo-Auth"), + xmlhttp.getResponseHeader("X-Echo-Auth") + ); + }); });