Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: http handler helper #7

Merged
merged 3 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
50 changes: 50 additions & 0 deletions http-handler.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/kesla/parse-headers/>
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
104 changes: 104 additions & 0 deletions test/http-handler.js
Original file line number Diff line number Diff line change
@@ -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();
});
5 changes: 5 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});