diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequest.js index c12888f087a146..31581f95648f46 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -423,12 +423,44 @@ class XMLHttpRequest extends (EventTarget(...XHR_EVENTS): any) { // according to the spec, return null if no response has been received return null; } - const headers = this.responseHeaders || {}; - return Object.keys(headers) - .map(headerName => { - return headerName + ': ' + headers[headerName]; - }) - .join('\r\n'); + + // Assign to non-nullable local variable. + const responseHeaders = this.responseHeaders; + + const unsortedHeaders = {}; + for (const rawHeaderName of Object.keys(responseHeaders)) { + const headerValue = responseHeaders[rawHeaderName]; + const lowerHeaderName = rawHeaderName.toLowerCase(); + if (unsortedHeaders.hasOwnProperty(lowerHeaderName)) { + unsortedHeaders[lowerHeaderName].headerValue += ', ' + headerValue; + } else { + unsortedHeaders[lowerHeaderName] = { + lowerHeaderName, + upperHeaderName: rawHeaderName.toUpperCase(), + headerValue, + }; + } + } + + // Sort in ascending order, with a being less than b if a's name is legacy-uppercased-byte less than b's name. + const sortedHeaders = Object.values(unsortedHeaders).sort((a, b) => { + if (a.upperHeaderName < b.upperHeaderName) { + return -1; + } + if (a.upperHeaderName > b.upperHeaderName) { + return 1; + } + return 0; + }); + + // Combine into single text response. + return ( + sortedHeaders + .map(header => { + return header.lowerHeaderName + ': ' + header.headerValue; + }) + .join('\r\n') + '\r\n' + ); } getResponseHeader(header: string): ?string { diff --git a/Libraries/Network/__tests__/XMLHttpRequest-test.js b/Libraries/Network/__tests__/XMLHttpRequest-test.js index 91d2ce1e252c98..f769d356f78881 100644 --- a/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -241,7 +241,7 @@ describe('XMLHttpRequest', function() { }); expect(xhr.getAllResponseHeaders()).toBe( - 'Content-Type: text/plain; charset=utf-8\r\n' + 'Content-Length: 32', + 'content-length: 32\r\n' + 'content-type: text/plain; charset=utf-8\r\n', ); }); @@ -292,4 +292,22 @@ describe('XMLHttpRequest', function() { ); expect(GlobalPerformanceLogger.stopTimespan).not.toHaveBeenCalled(); }); + + it('should sort and lowercase response headers', function() { + // Derived from XHR Web Platform Test: https://github.com/web-platform-tests/wpt/blob/master/xhr/getallresponseheaders.htm + xhr.open('GET', 'blabla'); + xhr.send(); + setRequestId(10); + xhr.__didReceiveResponse(requestId, 200, { + 'foo-TEST': '1', + 'FOO-test': '2', + __Custom: 'token', + 'ALSO-here': 'Mr. PB', + ewok: 'lego', + }); + + expect(xhr.getAllResponseHeaders()).toBe( + 'also-here: Mr. PB\r\newok: lego\r\nfoo-test: 1, 2\r\n__custom: token\r\n', + ); + }); });