From f6dd47dd1fdb3fc37f59fa934e15a9fe430100bb Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Thu, 22 Dec 2022 20:47:28 -0500 Subject: [PATCH] Zotero.HTTP.request(): Process headers case insensitively Using the Headers class from the Fetch API. Before, the added test would fail: `_requestInternal()`, not finding a header named `Content-Type` (case sensitive), would set it to `application/x-www-form-urlencoded`. XMLHttpRequest, upon being given both `content-type`: `application/json`) and `Content-Type`: `application/x-www-form-urlencoded`, would helpfully merge the two, producing `content-type`: `application/json, application/x-www-form-urlencoded`. That's obviously not the correct behavior. --- chrome/content/zotero/xpcom/http.js | 35 +++++++++++------------------ test/tests/httpTest.js | 28 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index ec12d83567..f73930ad68 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -117,7 +117,7 @@ Zotero.HTTP = new function() { * @param {nsIURI|String} url - URL to request * @param {Object} [options] Options for HTTP request: * @param {String} [options.body] - The body of a POST request - * @param {Object} [options.headers] - Object of HTTP headers to send with the request + * @param {Object | Headers} [options.headers] - HTTP headers to send with the request * @param {Boolean} [options.followRedirects = true] - Object of HTTP headers to send with the * request * @param {Zotero.CookieSandbox} [options.cookieSandbox] - The sandbox from which cookies should @@ -343,22 +343,19 @@ Zotero.HTTP = new function() { } // Send headers - var headers = {}; - if (options && options.headers) { - Object.assign(headers, options.headers); - } + var headers = new Headers(options?.headers || {}); var compressedBody = false; if (options.body) { - if (!headers["Content-Type"]) { - headers["Content-Type"] = "application/x-www-form-urlencoded"; + if (!headers.get("Content-Type")) { + headers.set("Content-Type", "application/x-www-form-urlencoded"); } - else if (headers["Content-Type"] == 'multipart/form-data') { + else if (headers.get("Content-Type") == 'multipart/form-data') { // Allow XHR to set Content-Type with boundary for multipart/form-data - delete headers["Content-Type"]; + headers.delete("Content-Type"); } if (options.compressBody && this.isWriteMethod(method)) { - headers['Content-Encoding'] = 'gzip'; + headers.set('Content-Encoding', 'gzip'); compressedBody = await Zotero.Utilities.Internal.gzip(options.body); let oldLen = options.body.length; @@ -368,23 +365,17 @@ Zotero.HTTP = new function() { } } if (options.debug) { - if (headers["Zotero-API-Key"]) { - let dispHeaders = {}; - Object.assign(dispHeaders, headers); - if (dispHeaders["Zotero-API-Key"]) { - dispHeaders["Zotero-API-Key"] = "[Not shown]"; - } - Zotero.debug(dispHeaders); + if (headers.has("Zotero-API-Key")) { + let dispHeaders = new Headers(headers); + dispHeaders.set("Zotero-API-Key", "[Not shown]"); + Zotero.debug({ ...dispHeaders.entries() }); } else { - Zotero.debug(headers); + Zotero.debug({ ...headers.entries() }); } } - for (var header in headers) { + for (var [header, value] of headers) { // Convert numbers to string to make Sinon happy - let value = typeof headers[header] == 'number' - ? headers[header].toString() - : headers[header] xmlhttp.setRequestHeader(header, value); } diff --git a/test/tests/httpTest.js b/test/tests/httpTest.js index dd03b59d18..5f9c2b1160 100644 --- a/test/tests/httpTest.js +++ b/test/tests/httpTest.js @@ -45,6 +45,20 @@ describe("Zotero.HTTP", function () { } } ); + httpd.registerPathHandler( + '/requireJSON', + { + handle(request, response) { + if (request.getHeader('Content-Type') == 'application/json') { + response.setStatusLine(null, 200, "OK"); + } + else { + response.setStatusLine(null, 400, "Bad Request"); + } + response.write('JSON required'); + } + } + ); }); beforeEach(function () { @@ -124,6 +138,20 @@ describe("Zotero.HTTP", function () { server.respond(); }); + it("should process headers case insensitively", async function () { + Zotero.HTTP.mock = null; + var req = await Zotero.HTTP.request( + 'GET', + baseURL + 'requireJSON', + { + headers: { + 'content-type': 'application/json' + } + } + ); + assert.equal(req.status, 200); + }); + describe("Retries", function () { var spy; var delayStub;