diff --git a/README.md b/README.md index ed55b2b..6d3e94c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # xhr -[![Join the chat at https://gitter.im/naugtur-xhr/Lobby](https://badges.gitter.im/naugtur-xhr/Lobby.svg)](https://gitter.im/naugtur-xhr/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - - +> Originally forked from [naugtur/xhr](https://github.com/naugtur/xhr). A small XMLHttpRequest wrapper. Designed for use with [browserify](http://browserify.org/), [webpack](https://webpack.github.io/) etc. @@ -76,6 +74,8 @@ the returned object is either an [`XMLHttpRequest`][3] instance or an [`XDomainRequest`][4] instance (if on IE8/IE9 && `options.useXDR` is set to `true`) +### XhrCallback + Your callback will be called once with the arguments ( [`Error`][5], `response` , `body` ) where the response is an object: ```js @@ -202,6 +202,52 @@ A function being called right before the `send` method of the `XMLHttpRequest` o Pass an `XMLHttpRequest` object (or something that acts like one) to use instead of constructing a new one using the `XMLHttpRequest` or `XDomainRequest` constructors. Useful for testing. +## Helpers + +This module exposes the following helpers on the `xhr` object. + +### `xhr.httpHandler(callback, decodeResponseBody) => XhrCallback` + +`httpHandler` is a wrapper for the [XhrCallback][] which returns an error for HTTP Status Codes 4xx and 5xx. Given a callback, it'll either return an error with the response body as the error's cause, or return the response body. + +Usage like so: +```js +xhr({ + uri: "https://example.com/foo", + responseType: 'arraybuffer' +}, xhr.httpHandler(function(err, responseBody) { + + // we got an error if the XHR errored out or if the status code was 4xx/5xx + if (err) { + // error cause is coming soon to JavaScript https://github.com/tc39/proposal-error-cause + throw new Error(err, {cause: err.cause}); + } + + // this will log an ArrayBuffer + console.log(responseBody); +}); +``` + +```js +xhr({ + uri: "https://example.com/foo", + responseType: 'arraybuffer' +}, xhr.httpHandler(function(err, responseBody) { + + if (err) { + throw new Error(err, {cause: err.cause}); + } + + // in this case, responseBody will be a String + console.log(responseBody); +}, + +// passing true as the second argument will cause httpHandler try and decode the response body into a string +true) +``` + + + ## FAQ - Why is my server's JSON response not parsed? I returned the right content-type. diff --git a/http-handler.js b/http-handler.js new file mode 100644 index 0000000..6255b27 --- /dev/null +++ b/http-handler.js @@ -0,0 +1,50 @@ +var window = require('global/window'); + +const httpResponseHandler = (callback, decodeResponseBody = false) => (err, response, responseBody) => { + // if the XHR failed, return that error + if (err) { + callback(err); + return; + } + + // if the HTTP status code is 4xx or 5xx, the request also failed + if (response.statusCode >= 400 && response.statusCode <= 599) { + let cause = responseBody; + + if (decodeResponseBody) { + if (window.TextDecoder) { + const charset = getCharset(response.headers && response.headers['content-type']); + + try { + cause = new TextDecoder(charset).decode(responseBody); + } catch (e) { + } + } else { + cause = String.fromCharCode.apply(null, new Uint8Array(responseBody)); + } + } + + callback({cause}); + return; + } + + // otherwise, request succeeded + callback(null, responseBody); +}; + +function getCharset(contentTypeHeader = '') { + return contentTypeHeader + .toLowerCase() + .split(';') + .reduce((charset, contentType) => { + const [type, value] = contentType.split('='); + + if (type.trim() === 'charset') { + return value.trim(); + } + + return charset; + }, 'utf-8'); +} + +module.exports = httpResponseHandler; diff --git a/index.d.ts b/index.d.ts index 0de08c2..812d756 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,13 @@ +export type BodyCallback = ( + error: Error, + body: any +) => void; + +export type HttpResponseHandler = ( + callback: BodyCallback, + decodeResponseBody: boolean +) => XhrCallback; + export type XhrCallback = ( error: Error, response: XhrResponse, diff --git a/index.js b/index.js index e2ca108..7382544 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,8 @@ var window = require("global/window") var _extends = require("@babel/runtime/helpers/extends"); var isFunction = require('is-function'); +createXHR.httpHandler = require('./http-handler.js'); + /** * @license * slighly modified parse-headers 2.0.2 diff --git a/package.json b/package.json index 038a4f3..30bd01e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ }, "license": "MIT", "scripts": { - "test": "run-browser test/index.js -b -m test/mock-server.js | tap-spec", + "test:index": "run-browser test/index.js -b -m test/mock-server.js | tap-spec", + "test:http-handler": "run-browser test/http-handler.js -b -m test/mock-server.js | tap-spec", + "test": "npm run test:index && npm run test:http-handler", "browser": "run-browser -m test/mock-server.js test/index.js" } } diff --git a/test/http-handler.js b/test/http-handler.js new file mode 100644 index 0000000..357e41d --- /dev/null +++ b/test/http-handler.js @@ -0,0 +1,104 @@ +var window = require("global/window") +var test = require("tape") +var forEach = require("for-each") + +var httpHandler = require("../http-handler.js") + +function toArrayBuffer(item) { + const buffer = new ArrayBuffer(item.length); + const bufferView = new Uint8Array(buffer); + + for (let i = 0; i < item.length; i++) { + bufferView[i] = item.charCodeAt(i); + } + return buffer; +} + +test('httpHandler takes a callback and returns a method of arity 3', function(assert) { + const xhrHandler = httpHandler(() => {}); + + assert.equal(xhrHandler.length, 3); + assert.end(); +}); + + +test('httpHandler returns responseBody to callback if no error and success http status code', function(assert) { + const xhrHandler = httpHandler((err, body) => { + assert.equal(body, 'hello'); + }); + + xhrHandler(null, { statusCode: 200 }, 'hello'); + assert.end(); +}); + +test('httpHandler passes error to callback', function(assert) { + const error = new Error('the error'); + + const xhrHandler = httpHandler((err, body) => { + assert.equal(err, error); + }); + + xhrHandler(error, null, 'hello'); + assert.end(); +}); + +test('httpHandler passes error to callback', function(assert) { + const error = new Error('the error'); + + const xhrHandler = httpHandler((err, body) => { + assert.equal(err, error); + }); + + xhrHandler(error, null, 'hello'); + assert.end(); +}); + +test('httpHandler returns responseBody as cause for 4xx/5xx responses', function(assert) { + const xhrHandler = httpHandler((err, body) => { + assert.equal(err.cause, "can't touch this"); + }); + + xhrHandler(null, { statusCode: 403 }, "can't touch this"); + xhrHandler(null, { statusCode: 504 }, "can't touch this"); + assert.end(); +}); + +test('httpHandler decodes responseBody using TextDecoder for 4xx/5xx responses', function(assert) { + const xhrHandler = httpHandler((err, body) => { + assert.equal(err.cause, "can't touch this"); + }, true); + + xhrHandler(null, { statusCode: 403 }, toArrayBuffer("can't touch this")); + xhrHandler(null, { statusCode: 504 }, toArrayBuffer("can't touch this")); + assert.end(); +}); + +test('httpHandler decodes responseBody using TextDecoder for 4xx/5xx responses', function(assert) { + let xhrHandler = httpHandler((err, body) => { + assert.equal(err.cause, ""); + }, true); + + xhrHandler(null, { statusCode: 403 }, toArrayBuffer("")); + + xhrHandler = httpHandler((err, body) => { + assert.equal(err.cause, null); + }, true); + xhrHandler(null, { statusCode: 504 }, null); + assert.end(); +}); + +test('httpHandler decodes responseBody using fromCharCode if TextDecoder is unavailable for 4xx/5xx responses', function(assert) { + const TextDecoder = window.TextDecoder; + + window.TextDecoder = null; + + const xhrHandler = httpHandler((err, body) => { + assert.equal(err.cause, "can't touch this"); + }, true); + + xhrHandler(null, { statusCode: 403 }, toArrayBuffer("can't touch this")); + xhrHandler(null, { statusCode: 504 }, toArrayBuffer("can't touch this")); + + window.TextDecoder = TextDecoder; + assert.end(); +}); diff --git a/test/index.js b/test/index.js index d8d1e68..df02fc7 100644 --- a/test/index.js +++ b/test/index.js @@ -376,3 +376,8 @@ test("XHR can be overridden", { timeout: 500 }, function(assert) { assert.equal(xdrs, 1, "created the custom XDR") assert.end() }) + +test('httpHandler is available on XHR', function(assert) { + assert.ok(xhr.httpHandler) + assert.end(); +});