From c5f3a413bc00acf9ac1046fb15b454096a8890c6 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 29 Jun 2011 00:25:13 -0700 Subject: feat:$xhr: provide access to $xhr header defaults $xhr header defaults are now exposed as $xhr.defaults.headers.common and $xhr.default.headers.. This allows applications to configure their defaults as needed. This commit doesn't allow headers to be set per request, only per application. Per request change would require api change, which I tried to avoid *for now*. --- CHANGELOG.md | 1 + src/Browser.js | 11 +---- src/service/xhr.js | 42 ++++++++++++++-- test/BrowserSpecs.js | 59 ++++++++--------------- test/service/xhrSpec.js | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6febc7..d9c926f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - New [ng:disabled], [ng:selected], [ng:checked], [ng:multiple] and [ng:readonly] directives. - Added support for string representation of month and day in [date] filter. - Added support for `prepend()` to [jqLite]. +- Added support for configurable HTTP header defaults for the [$xhr] service. ### Bug Fixes diff --git a/src/Browser.js b/src/Browser.js index 5a675e3c..37fb4931 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -8,14 +8,6 @@ var XHR = window.XMLHttpRequest || function () { throw new Error("This browser does not support XMLHttpRequest."); }; -// default xhr headers -var XHR_HEADERS = { - DEFAULT: { - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest" - }, - POST: {'Content-Type': 'application/x-www-form-urlencoded'} -}; /** * @private @@ -108,8 +100,7 @@ function Browser(window, document, body, XHR, $log) { } else { var xhr = new XHR(); xhr.open(method, url, true); - forEach(extend({}, XHR_HEADERS.DEFAULT, XHR_HEADERS[uppercase(method)] || {}, headers || {}), - function(value, key) { + forEach(headers, function(value, key) { if (value) xhr.setRequestHeader(key, value); }); xhr.onreadystatechange = function() { diff --git a/src/service/xhr.js b/src/service/xhr.js index 62b27263..d26cda42 100644 --- a/src/service/xhr.js +++ b/src/service/xhr.js @@ -24,6 +24,22 @@ * and process it in application specific way, or resume normal execution by calling the * request callback method. * + * # HTTP Headers + * The $xhr service will automatically add certain http headers to all requests. These defaults can + * be fully configured by accessing the `$xhr.defaults.headers` configuration object, which + * currently contains this default configuration: + * + * - `$xhr.defaults.headers.common` (headers that are common for all requests): + * - `Accept: application/json, text/plain, *\/*` + * - `X-Requested-With: XMLHttpRequest` + * - `$xhr.defaults.headers.post` (header defaults for HTTP POST requests): + * - `Content-Type: application/x-www-form-urlencoded` + * + * To add or overwrite these defaults, simple add or remove a property from this configuration + * object. To add headers for an HTTP method other than POST, simple create a new object with name + * equal to the lowercased http method name, e.g. `$xhr.defaults.headers.get['My-Header']='value'`. + * + * * # Security Considerations * When designing web applications your design needs to consider security threats from * {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx @@ -126,7 +142,21 @@ */ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ - return function(method, url, post, callback){ + + var xhrHeaderDefaults = { + common: { + "Accept": "application/json, text/plain, */*", + "X-Requested-With": "XMLHttpRequest" + }, + post: {'Content-Type': 'application/x-www-form-urlencoded'}, + get: {}, // all these empty properties are needed so that client apps can just do: + head: {}, // $xhr.defaults.headers.head.foo="bar" without having to create head object + put: {}, // it also means that if we add a header for these methods in the future, it + 'delete': {}, // won't be easily silently lost due to an object assignment. + patch: {} + }; + + function xhr(method, url, post, callback){ if (isFunction(post)) { callback = post; post = null; @@ -155,8 +185,12 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){ } finally { $updateView(); } - }, { - 'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN'] - }); + }, extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, + xhrHeaderDefaults.common, + xhrHeaderDefaults[lowercase(method)])); }; + + xhr.defaults = {headers: xhrHeaderDefaults}; + + return xhr; }, ['$browser', '$xhr.error', '$log', '$updateView']); diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js index 08756904..cb59137e 100644 --- a/test/BrowserSpecs.js +++ b/test/BrowserSpecs.js @@ -100,31 +100,6 @@ describe('browser', function(){ }); }); - it('should set headers for all requests', function(){ - var code, response, headers = {}; - browser.xhr('GET', 'URL', 'POST', function(c,r){ - code = c; - response = r; - }, {'X-header': 'value'}); - - expect(xhr.method).toEqual('GET'); - expect(xhr.url).toEqual('URL'); - expect(xhr.post).toEqual('POST'); - expect(xhr.headers).toEqual({ - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest", - "X-header":"value" - }); - - xhr.status = 202; - xhr.responseText = 'RESPONSE'; - xhr.readyState = 4; - xhr.onreadystatechange(); - - expect(code).toEqual(202); - expect(response).toEqual('RESPONSE'); - }); - it('should normalize IE\'s 1223 status code into 204', function() { var callback = jasmine.createSpy('XHR'); @@ -138,24 +113,28 @@ describe('browser', function(){ expect(callback.argsForCall[0][0]).toEqual(204); }); - it('should not set Content-type header for GET requests', function() { - browser.xhr('GET', 'URL', 'POST-DATA', function(c, r) {}); - - expect(xhr.headers['Content-Type']).not.toBeDefined(); - }); - - it('should set Content-type header for POST requests', function() { - browser.xhr('POST', 'URL', 'POST-DATA', function(c, r) {}); + it('should set only the requested headers', function() { + var code, response, headers = {}; + browser.xhr('POST', 'URL', null, function(c,r){ + code = c; + response = r; + }, {'X-header1': 'value1', 'X-header2': 'value2'}); - expect(xhr.headers['Content-Type']).toBeDefined(); - expect(xhr.headers['Content-Type']).toEqual('application/x-www-form-urlencoded'); - }); + expect(xhr.method).toEqual('POST'); + expect(xhr.url).toEqual('URL'); + expect(xhr.post).toEqual(''); + expect(xhr.headers).toEqual({ + "X-header1":"value1", + "X-header2":"value2" + }); - it('should set default headers for custom methods', function() { - browser.xhr('CUSTOM', 'URL', 'POST-DATA', function(c, r) {}); + xhr.status = 202; + xhr.responseText = 'RESPONSE'; + xhr.readyState = 4; + xhr.onreadystatechange(); - expect(xhr.headers['Accept']).toEqual('application/json, text/plain, */*'); - expect(xhr.headers['X-Requested-With']).toEqual('XMLHttpRequest'); + expect(code).toEqual(202); + expect(response).toEqual('RESPONSE'); }); }); diff --git a/test/service/xhrSpec.js b/test/service/xhrSpec.js index ebcd90d4..1f31bb6f 100644 --- a/test/service/xhrSpec.js +++ b/test/service/xhrSpec.js @@ -102,6 +102,131 @@ describe('$xhr', function() { expect(response).toEqual([1, 'abc', {foo:'bar'}]); }); + + describe('http headers', function() { + + describe('default headers', function() { + + it('should set default headers for GET request', function(){ + var callback = jasmine.createSpy('callback'); + + $browserXhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest'}). + respond(234, 'OK'); + + $xhr('GET', 'URL', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + }); + + + it('should set default headers for POST request', function(){ + var callback = jasmine.createSpy('callback'); + + $browserXhr.expectPOST('URL', 'xx', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded'}). + respond(200, 'OK'); + + $xhr('POST', 'URL', 'xx', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + }); + + + it('should set default headers for custom HTTP method', function(){ + var callback = jasmine.createSpy('callback'); + + $browserXhr.expect('FOO', 'URL', '', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest'}). + respond(200, 'OK'); + + $xhr('FOO', 'URL', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + }); + + + describe('custom headers', function() { + + it('should allow appending a new header to the common defaults', function() { + var callback = jasmine.createSpy('callback'); + + $browserXhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest', + 'Custom-Header': 'value'}). + respond(200, 'OK'); + + $xhr.defaults.headers.common['Custom-Header'] = 'value'; + $xhr('GET', 'URL', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + callback.reset(); + + $browserXhr.expectPOST('URL', 'xx', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Custom-Header': 'value'}). + respond(200, 'OK'); + + $xhr('POST', 'URL', 'xx', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + }); + + + it('should allow appending a new header to a method specific defaults', function() { + var callback = jasmine.createSpy('callback'); + + $browserXhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json'}). + respond(200, 'OK'); + + $xhr.defaults.headers.get['Content-Type'] = 'application/json'; + $xhr('GET', 'URL', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + callback.reset(); + + $browserXhr.expectPOST('URL', 'x', {'Accept': 'application/json, text/plain, */*', + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded'}). + respond(200, 'OK'); + + $xhr('POST', 'URL', 'x', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + }); + + + it('should support overwriting and deleting default headers', function() { + var callback = jasmine.createSpy('callback'); + + $browserXhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*'}). + respond(200, 'OK'); + + //delete a default header + delete $xhr.defaults.headers.common['X-Requested-With']; + $xhr('GET', 'URL', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + callback.reset(); + + $browserXhr.expectPOST('URL', 'xx', {'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json'}). + respond(200, 'OK'); + + //overwrite a default header + $xhr.defaults.headers.post['Content-Type'] = 'application/json'; + $xhr('POST', 'URL', 'xx', callback); + $browserXhr.flush(); + expect(callback).toHaveBeenCalled(); + }); + }); + }); + }); + describe('xsrf', function(){ it('should copy the XSRF cookie into a XSRF Header', function(){ var code, response; -- cgit v1.2.3