├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── jsonist.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | node_js: 4 | - 8 5 | - 10 6 | - 12 7 | - lts/* 8 | - current 9 | branches: 10 | only: 11 | - master 12 | notifications: 13 | email: 14 | - rod@vagg.org 15 | script: npm test 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, Rod Vagg (the "Original Author") 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | Distributions of all or part of the Software intended to be used 17 | by the recipients as they would use the unmodified Software, 18 | containing modifications that substantially alter, remove, or 19 | disable functionality of the Software, outside of the documented 20 | configuration mechanisms provided by the Software, shall be 21 | modified such that the Original Author's bug reporting email 22 | addresses and urls are either replaced with the contact information 23 | of the parties responsible for the changes, or removed entirely. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 27 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 29 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | 35 | Except where noted, this license applies to any and all software 36 | programs and associated documentation files created by the 37 | Original Author, when distributed with the Software. 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonist 2 | 3 | [![Build Status](https://api.travis-ci.org/rvagg/jsonist.svg?branch=master)](http://travis-ci.org/rvagg/jsonist) 4 | 5 | **A super-simple HTTP fetch utility for JSON APIs** 6 | 7 | [![NPM](https://nodei.co/npm/jsonist.svg)](https://nodei.co/npm/jsonist/) 8 | 9 | * [Example](#example) 10 | * [API](#api) 11 | * [jsonist.get(url[, options ][, callback ])](#jsonistgeturl-options--callback-) 12 | * [jsonist.post(url, data[, options ][, callback ])](#jsonistposturl-data-options--callback-) 13 | * [jsonist.put(url, data[, options ][, callback ])](#jsonistputurl-data-options--callback-) 14 | * [jsonist.delete(url[, options ][, callback ])](#jsonistdeleteurl-options--callback-) 15 | * [Error handling and bad JSON responses](#error-handling-and-bad-json-responses) 16 | * [License & copyright](#license--copyright) 17 | 18 | ## Example 19 | 20 | A simple GET: 21 | 22 | ```js 23 | const url = 'https://api.github.com/users/rvagg' 24 | const opts = { headers: { 'user-agent': 'wascally wabbit' } } 25 | 26 | const { data } = await jsonist.get(url, opts) 27 | 28 | console.log(`${data.name} (@${data.login}) is: ${data.bio}`) 29 | 30 | // → Rod Vagg (@rvagg) is: Awk Ninja; Yak Shaving Rock Star 31 | ``` 32 | 33 | or a POST: 34 | 35 | ```js 36 | const url = 'https://api.github.com/repos/rvagg/jsonist/issues' 37 | const opts = { 38 | headers: { 'user-agent': 'yee haw grandma' }, 39 | auth: 'rvagg:24d5dee258c64aef38a66c0c5eca459c379901c2' 40 | } 41 | const data = { 42 | 'title': 'Not a bug' 43 | 'body': 'Just guinea-pigging your repo, don\'t get so uptight.' 44 | } 45 | const { data } = await jsonist.post(url, data, opts, fn) 46 | console.log(data) 47 | 48 | // → { url: 'https://api.github.com/repos/rvagg/jsonist/issues/1', 49 | // ... 50 | // } 51 | 52 | // you can also jsonist.put(), the kids love PUT requests these days 53 | ``` 54 | 55 | You can use the `Promise` API for async / await, or steer clear entirely of Promises and provide a `callback` argument (in which case there won't be any `Promise` in your stack to ruin your error handling). 56 | 57 | **jsonist** uses [hyperquest](https://github.com/substack/hyperquest) under the hood, `options` for the API below where present are passed on to hyperquest. 58 | 59 | ## API 60 | 61 | ### jsonist.get(url[, options ][, callback ]) 62 | 63 | Sends a GET request to `url` and returns (via `callback` if supplied or a returned `Promise` if not) an error or JSON deserialised data. 64 | 65 | The `options` object is optional and is passed on to hyperquest where present: 66 | 67 | * `followRedirects` (default `false`): if truthy, jsonist will follow HTTP redirects to new locations, up to a maximum of `10` times. Set `followRedirects` to an integer to change the maximum number of redirects to follow. 68 | * `hyperquest`: if provided, will be used in place of the bare hyperquest package. This can be used to customise the HTTP chain with a hyperquest wrapper, such as those at [github.com/hyperquest](https://github.com/hyperquest). Use with caution. 69 | 70 | Options understood by hyperquest include: 71 | 72 | * `headers` (default `{}`, in addition, jsonist will set `content-type` to `'application/json'` and `accept` to `'application/json'`): any additional headers required for the request. 73 | * `auth` (default `undefined`): set automatically when the `url` has an auth string in it such as "http://user:passwd@host". Set to a string of the form `"user:pass"` where auth is required. 74 | * `agent` (default `false`): can be set to a custom [`http.Agent`](https://nodejs.org/api/http.html#http_class_http_agent). 75 | * `timeout` (default `2``32`` * 1000`): set on the underlying `request.setTimeout()`. 76 | * `localAddress`: the local interface to bind for network connections when issuing the request. 77 | 78 | For HTTPS connections, the following options are passed on to [`tls.connect()`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback): 79 | 80 | * `pfx` 81 | * `key` 82 | * `cert` 83 | * `ca` 84 | * `ciphers` 85 | * `rejectUnauthorized` 86 | * `secureProtocol` 87 | 88 | If a `callback` is supplied, it will be called with up to 3 arguments. If there is an error there will only be an error argument in the first position, otherwise it will be `null`. The second argument will contain the deserialised object obtained from the server and the third argument will be the response object itself if you need to fetch headers or other metadata. 89 | 90 | When a `callback` is supplied, `jsonist.get()` will immediately return the underlying hyperquest stream for this request. Can be safely ignored in most circumstances. This is not available on the non-callback version. 91 | 92 | If no `callback` is supplied, a `Promise` is returned directly, allowing for `await`. If the `Promise` resolves, it will receive an object with a `data` property containing the deserialised object obtained from the server, and a `response` property containing the response object itself if you need to fetch headers or other metadata. These two properties can be destructured with `const { data, response } = await jsonist.get(...)`. 93 | 94 | ### jsonist.post(url, data[, options ][, callback ]) 95 | 96 | Sends a POST request to `url`, writing JSON serialised data to the request, and returns (via `callback` if supplied or a returned `Promise` if not) an error or JSON deserialised data (if any). 97 | 98 | `'method'` is set to `'POST'` for you before passing on to hyperquest. 99 | 100 | The `data` parameter can also be a readable stream that will get `.pipe()`'d to the request. 101 | 102 | See [`jsonist.get()`](#jsonistgeturl-options--callback-) for more details on options and the behaviour when passing a `callback` or using the `Promise` version. 103 | 104 | ### jsonist.put(url, data[, options ][, callback ]) 105 | 106 | Same as `jsonist.post()` but for when that extra character is too much to type or you have to use someone's overloaded API. `'method'` is set to `'PUT'`. 107 | 108 | See [`jsonist.get()`](#jsonistgeturl-options--callback-) for more details on options and the behaviour when passing a `callback` or using the `Promise` version. 109 | 110 | ### jsonist.delete(url[, options ][, callback ]) 111 | 112 | Sends a DELETE request to `url` and returns (via `callback` if supplied or a returned `Promise` if not) an error or JSON deserialised data. 113 | 114 | Otherwise works the same as GET. 115 | 116 | See [`jsonist.get()`](#jsonistgeturl-options--callback-) for more details on options and the behaviour when passing a `callback` or using the `Promise` version. 117 | 118 | ## Error handling and bad JSON responses 119 | 120 | Server errors (i.e. response codes >= 300) are handled as standard responses. You can get the status code from the response object which is the third argument to the standard callback if you need to handle error responses in a different way. 121 | 122 | However, if any type of response returns data that is not JSON format, an error will be generated and passed as the first argument on the callback, with the following customisations: 123 | 124 | * If the status code from the server is >= 300, you will receive an error of type `jsonist.HttpError`, otherwise it will be of type `SyntaxError` indicating a bad JSON parse on a normal response. 125 | * The error will come with the following additional properties attached: 126 | - `data`: a `Buffer` containing the full response from the server 127 | - `response`: the full HTTP response object 128 | - `statusCode`: the status code received from the server (a short-cut to `response.statusCode`) 129 | 130 | ## License & copyright 131 | 132 | **jsonist** is Copyright (c) 2014 Rod Vagg [@rvagg](https://github.com/rvagg) and licensed under the MIT licence. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE file for more details. 133 | -------------------------------------------------------------------------------- /jsonist.js: -------------------------------------------------------------------------------- 1 | const URL = require('url').URL 2 | const hyperquest = require('hyperquest') 3 | const bl = require('bl') 4 | const stringify = require('json-stringify-safe') 5 | 6 | function HttpError (message) { 7 | SyntaxError.call(this, message) 8 | Error.captureStackTrace(this, arguments.callee) // eslint-disable-line 9 | } 10 | 11 | HttpError.prototype = Object.create(SyntaxError.prototype) 12 | HttpError.prototype.constructor = HttpError 13 | 14 | function collector (uri, options, callback) { 15 | const request = makeRequest(uri, options) 16 | let redirect = null 17 | let redirectCount = 0 18 | 19 | return handle(request) 20 | 21 | function handle (request) { 22 | if (options.followRedirects) { 23 | request.on('response', (response) => { 24 | redirect = isRedirect(request.request, response) && response.headers.location 25 | }) 26 | } 27 | 28 | request.pipe(bl((err, data) => { 29 | if (redirect) { 30 | if (++redirectCount >= (typeof options.followRedirects === 'number' ? options.followRedirects : 10)) { 31 | return callback(new Error('Response was redirected too many times (' + redirectCount + ')')) 32 | } 33 | request = makeRequest(new URL(redirect, uri).toString(), options) 34 | redirect = null 35 | return handle(request) 36 | } 37 | 38 | if (err) { 39 | return callback(err) 40 | } 41 | 42 | if (!data.length) { 43 | return callback(null, null, request.response) 44 | } 45 | 46 | let ret, msg 47 | 48 | try { 49 | ret = JSON.parse(data.toString()) 50 | } catch (e) { 51 | msg = 'JSON parse error: ' + e.message 52 | err = request.response.statusCode >= 300 ? new HttpError(msg) : new SyntaxError(msg) 53 | 54 | err.statusCode = request.response.statusCode 55 | err.data = data 56 | err.response = request.response 57 | 58 | return callback(err) 59 | } 60 | 61 | callback(null, ret, request.response) 62 | })) 63 | 64 | return request 65 | } 66 | } 67 | 68 | function makeMethod (method, data) { 69 | function handler (uri, options, callback) { 70 | const defaultOptions = { method, headers: {} } 71 | if (typeof options === 'object') { 72 | options = Object.assign(defaultOptions, options) 73 | } else { 74 | callback = options 75 | options = defaultOptions 76 | } 77 | 78 | if (data && typeof options.headers['content-type'] !== 'string') { 79 | options.headers['content-type'] = 'application/json' 80 | } 81 | if (typeof options.headers.accept !== 'string') { 82 | options.headers.accept = 'application/json' 83 | } 84 | 85 | return collector(uri, options, callback) 86 | } 87 | 88 | function dataHandler (uri, data, options, callback) { 89 | const request = handler(uri, options, callback) 90 | 91 | if (typeof data.pipe === 'function') { 92 | data.pipe(request) 93 | } else { 94 | request.end(stringify(data)) 95 | } 96 | 97 | return request 98 | } 99 | 100 | return data ? dataHandler : handler 101 | } 102 | 103 | function makeRequest (uri, options) { 104 | return (options.hyperquest || hyperquest)(uri, options) 105 | } 106 | 107 | function isRedirect (request, response) { 108 | return request.method === 'GET' && 109 | response.headers.location && 110 | (response.statusCode === 301 || 111 | response.statusCode === 302 || 112 | response.statusCode === 307 || 113 | response.statusCode === 308 114 | ) 115 | } 116 | 117 | function maybePromisify (fn) { 118 | return function jsonistMaybePromise (...args) { 119 | if (typeof args[args.length - 1] !== 'function') { // no callback 120 | return new Promise((resolve, reject) => { 121 | this.request = fn.call(this, ...args, (err, data, response) => { 122 | if (err) { 123 | return reject(err) 124 | } 125 | resolve({ data, response }) 126 | }) 127 | }) 128 | } else { 129 | return fn.call(this, ...args) 130 | } 131 | } 132 | } 133 | 134 | module.exports.get = maybePromisify(makeMethod('GET', false)) 135 | module.exports.post = maybePromisify(makeMethod('POST', true)) 136 | module.exports.put = maybePromisify(makeMethod('PUT', true)) 137 | module.exports.delete = maybePromisify(function deleteHandler (uri, options, callback) { 138 | // behaves half-way between a data posting request and a GET 139 | // since https://github.com/substack/hyperquest/commit/9b130e101 140 | return makeMethod('DELETE', false)(uri, options, callback).end() 141 | }) 142 | module.exports.HttpError = HttpError 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonist", 3 | "version": "3.0.1", 4 | "description": "A simple wrapper around for dealing with JSON web APIs", 5 | "main": "jsonist.js", 6 | "scripts": { 7 | "lint": "standard *.js", 8 | "ci": "npm run lint && node test.js", 9 | "test": "npm run lint && node test.js | faucet" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/rvagg/jsonist.git" 14 | }, 15 | "keywords": [ 16 | "http", 17 | "hyperquest", 18 | "json", 19 | "hungry hungry http hippo!" 20 | ], 21 | "author": "Rod Vagg ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/rvagg/jsonist/issues" 25 | }, 26 | "dependencies": { 27 | "bl": "~4.0.0", 28 | "hyperquest": "~2.1.3", 29 | "json-stringify-safe": "~5.0.1" 30 | }, 31 | "homepage": "https://github.com/rvagg/jsonist", 32 | "devDependencies": { 33 | "after": "~0.8.2", 34 | "faucet": "~0.0.1", 35 | "standard": "~14.3.1", 36 | "tape": "~4.11.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const http = require('http') 3 | const fs = require('fs') 4 | const bl = require('bl') 5 | const EE = require('events').EventEmitter 6 | const jsonist = require('./') 7 | const stringify = require('json-stringify-safe') 8 | const after = require('after') 9 | 10 | function testServer (serverResponse, statusCode) { 11 | const ee = new EE() 12 | const server = http.createServer(handler) 13 | 14 | function handler (req, res) { 15 | req.pipe(bl((err, data) => { 16 | if (err) { return ee.emit('error', err) } 17 | 18 | ee.emit('request', req, data.toString()) 19 | 20 | setTimeout(server.close.bind(server), 20) 21 | })) 22 | 23 | res.writeHead( 24 | typeof statusCode === 'number' ? statusCode : 200 25 | , { 'content-type': 'application/json' } 26 | ) 27 | res.end(serverResponse || '') 28 | } 29 | 30 | server.listen(() => { 31 | ee.emit('ready', 'http://localhost:' + server.address().port) 32 | }) 33 | 34 | server.on('close', ee.emit.bind(ee, 'close')) 35 | 36 | return ee 37 | } 38 | 39 | for (const type of ['get', 'delete']) { 40 | for (const promise of [true, false]) { 41 | test(`${type} fetch json doc with ${promise ? 'Promise' : 'callback'}`, (t) => { 42 | t.plan(7) 43 | 44 | const testDoc = { a: 'test', doc: true, arr: [{ of: 'things' }] } 45 | 46 | function verify (data, response) { 47 | t.deepEqual(data, testDoc, 'got correct doc') 48 | t.ok(response, 'got response object') 49 | t.equal( 50 | response && response.headers && response.headers['content-type'] 51 | , 'application/json', 'verified response object by content-type header' 52 | ) 53 | } 54 | 55 | testServer(stringify(testDoc)) 56 | .on('ready', (url) => { 57 | if (promise) { 58 | jsonist[type](url).then(({ data, response }) => { 59 | verify(data, response) 60 | }).catch((err) => { 61 | t.ifError(err) 62 | }) 63 | t.ok(true) // account for callback ifError() 64 | } else { 65 | jsonist[type](url, (err, data, response) => { 66 | t.ifError(err) 67 | verify(data, response) 68 | }) 69 | } 70 | }) 71 | .on('request', (req, data) => { 72 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 73 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 74 | }) 75 | .on('close', t.ok.bind(t, true, 'ended')) 76 | }) 77 | 78 | test(`${type} fetch non-json doc with ${promise ? 'Promise' : 'callback'}`, (t) => { 79 | t.plan(promise ? 4 : 7) 80 | 81 | testServer('this is not json') 82 | .on('ready', (url) => { 83 | if (promise) { 84 | jsonist[type](url).then(() => { 85 | t.fail('should have errored') 86 | }).catch((err) => { 87 | t.ok(/JSON/.test(err.message), 'error says something about "JSON"') 88 | }) 89 | } else { 90 | jsonist[type](url, (err, data, response) => { 91 | t.ok(err, 'got error') 92 | t.notOk(data, 'no data') 93 | t.notOk(response, 'no response obj') 94 | t.ok(/JSON/.test(err.message), 'error says something about "JSON"') 95 | }) 96 | } 97 | }) 98 | .on('request', (req, data) => { 99 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 100 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 101 | }) 102 | .on('close', t.ok.bind(t, true, 'ended')) 103 | }) 104 | } 105 | } 106 | 107 | for (const type of ['post', 'put']) { 108 | for (const promise of [true, false]) { 109 | test(`${type} json, no response with ${promise ? 'Promise' : 'callback'}`, (t) => { 110 | t.plan(9) 111 | 112 | const testDoc = { a: 'test2', doc: true, arr: [{ of: 'things' }] } 113 | 114 | function verify (data, response) { 115 | t.notOk(data, 'no data') 116 | t.ok(response, 'got response object') 117 | t.equal( 118 | response && response.headers && response.headers['content-type'] 119 | , 'application/json', 'verified response object by content-type header' 120 | ) 121 | } 122 | 123 | testServer('') 124 | .on('ready', (url) => { 125 | if (promise) { 126 | jsonist[type](url, Object.assign(testDoc)).then(({ data, response }) => { 127 | verify(data, response) 128 | }).catch((err) => { 129 | t.ifError(err) 130 | }) 131 | t.ok(true) // account for t.ifError() on callback plan 132 | } else { 133 | jsonist[type](url, Object.assign(testDoc), (err, data, response) => { 134 | t.ifError(err) 135 | verify(data, response) 136 | }) 137 | } 138 | }) 139 | .on('request', (req, data) => { 140 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 141 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 142 | t.equal(req.headers['content-type'], 'application/json', 'got correct encoding') 143 | t.deepEqual(JSON.parse(data), testDoc, 'got correct ' + type) 144 | }) 145 | .on('close', t.ok.bind(t, true, 'ended')) 146 | }) 147 | 148 | test(`${type} json, with response with ${promise ? 'Promise' : 'callback'}`, (t) => { 149 | t.plan(9) 150 | 151 | const sendDoc = { a: 'test2', doc: true, arr: [{ of: 'things' }] } 152 | const recvDoc = { recv: 'this', obj: true } 153 | 154 | function verify (data, response) { 155 | t.deepEqual(data, recvDoc) 156 | t.ok(response, 'got response object') 157 | t.equal( 158 | response && response.headers && response.headers['content-type'] 159 | , 'application/json', 'verified response object by content-type header' 160 | ) 161 | } 162 | 163 | testServer(stringify(recvDoc)) 164 | .on('ready', (url) => { 165 | if (promise) { 166 | jsonist[type](url, Object.assign(sendDoc)).then(({ data, response }) => { 167 | verify(data, response) 168 | }).catch((err) => { 169 | t.ifError(err) 170 | }) 171 | t.ok(true) // account for t.ifError() on callback plan 172 | } else { 173 | jsonist[type](url, Object.assign(sendDoc), (err, data, response) => { 174 | t.ifError(err) 175 | verify(data, response) 176 | }) 177 | } 178 | }) 179 | .on('request', (req, data) => { 180 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 181 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 182 | t.equal(req.headers['content-type'], 'application/json', 'got correct encoding') 183 | t.deepEqual(JSON.parse(data), sendDoc, 'got correct ' + type) 184 | }) 185 | .on('close', t.ok.bind(t, true, 'ended')) 186 | }) 187 | 188 | test(`${type} data with no pipe function treated as data with ${promise ? 'Promise' : 'callback'}`, (t) => { 189 | t.plan(9) 190 | 191 | const sendDoc = { 192 | a: 'test2', 193 | doc: true, 194 | arr: [{ of: 'things' }], 195 | pipe: 'this is a string and not a function' 196 | } 197 | const recvDoc = { recv: 'this', obj: true } 198 | 199 | function verify (data, response) { 200 | t.deepEqual(data, recvDoc) 201 | t.ok(response, 'got response object') 202 | t.equal( 203 | response && response.headers && response.headers['content-type'] 204 | , 'application/json', 'verified response object by content-type header' 205 | ) 206 | } 207 | 208 | testServer(stringify(recvDoc)) 209 | .on('ready', (url) => { 210 | if (promise) { 211 | jsonist[type](url, Object.assign(sendDoc)).then(({ data, response }) => { 212 | verify(data, response) 213 | }).catch((err) => { 214 | t.ifError(err) 215 | }) 216 | t.ok(true) // account for t.ifError() on callback plan 217 | } else { 218 | jsonist[type](url, Object.assign(sendDoc), (err, data, response) => { 219 | t.ifError(err) 220 | verify(data, response) 221 | }) 222 | } 223 | }) 224 | .on('request', (req, data) => { 225 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 226 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 227 | t.equal(req.headers['content-type'], 'application/json', 'got correct encoding') 228 | t.deepEqual(JSON.parse(data), sendDoc, 'got correct ' + type) 229 | }) 230 | .on('close', t.ok.bind(t, true, 'ended')) 231 | }) 232 | 233 | test(`${type} data with pipe function will data.pipe(req) with ${promise ? 'Promise' : 'callback'}`, (t) => { 234 | t.plan(10) 235 | 236 | const sendDoc = { 237 | a: 'test2', 238 | doc: true, 239 | arr: [{ of: 'things' }] 240 | } 241 | const stream = { 242 | pipe: (req) => { 243 | t.ok(req, 'request should be set') 244 | req.end(stringify(sendDoc)) 245 | } 246 | } 247 | const recvDoc = { recv: 'this', obj: true } 248 | 249 | function verify (data, response) { 250 | t.deepEqual(data, recvDoc) 251 | t.ok(response, 'got response object') 252 | t.equal( 253 | response && response.headers && response.headers['content-type'] 254 | , 'application/json', 'verified response object by content-type header' 255 | ) 256 | } 257 | 258 | testServer(stringify(recvDoc)) 259 | .on('ready', (url) => { 260 | if (promise) { 261 | jsonist[type](url, stream).then(({ data, response }) => { 262 | verify(data, response) 263 | }).catch((err) => { 264 | t.ifError(err) 265 | }) 266 | t.ok(true) // account for t.ifError() on callback plan 267 | } else { 268 | jsonist[type](url, stream, (err, data, response) => { 269 | t.ifError(err) 270 | verify(data, response) 271 | }) 272 | } 273 | }) 274 | .on('request', (req, data) => { 275 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 276 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 277 | t.equal(req.headers['content-type'], 'application/json', 'got correct encoding') 278 | t.deepEqual(JSON.parse(data), sendDoc, 'got correct ' + type) 279 | }) 280 | .on('close', t.ok.bind(t, true, 'ended')) 281 | }) 282 | 283 | test(`${type} data with pipe function and real stream works with ${promise ? 'Promise' : 'callback'}`, (t) => { 284 | t.plan(9) 285 | 286 | const file = `${__dirname}/package.json` 287 | const content = JSON.parse(fs.readFileSync(file)) 288 | const stream = fs.createReadStream(file) 289 | const recvDoc = { recv: 'this', obj: true } 290 | 291 | function verify (data, response) { 292 | t.deepEqual(data, recvDoc) 293 | t.ok(response, 'got response object') 294 | t.equal( 295 | response && response.headers && response.headers['content-type'] 296 | , 'application/json', 'verified response object by content-type header' 297 | ) 298 | } 299 | 300 | testServer(stringify(recvDoc)) 301 | .on('ready', (url) => { 302 | if (promise) { 303 | jsonist[type](url, stream).then(({ data, response }) => { 304 | verify(data, response) 305 | }).catch((err) => { 306 | t.ifError(err) 307 | }) 308 | t.ok(true) // account for t.ifError() on callback plan 309 | } else { 310 | jsonist[type](url, stream, (err, data, response) => { 311 | t.ifError(err) 312 | verify(data, response) 313 | }) 314 | } 315 | }) 316 | .on('request', (req, data) => { 317 | t.equal(req.method, type.toUpperCase(), `got ${type} request`) 318 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 319 | t.equal(req.headers['content-type'], 'application/json', 'got correct encoding') 320 | t.deepEqual(JSON.parse(data), content, 'got correct ' + type) 321 | }) 322 | .on('close', t.ok.bind(t, true, 'ended')) 323 | }) 324 | } 325 | } 326 | 327 | test('follow redirect', (t) => { 328 | t.plan(7) 329 | 330 | const expectedResponse = { ok: 'foobar!' } 331 | const server = http.createServer((req, res) => { 332 | if (req.url === '/') { // 2 requests come in here 333 | t.ok('got /') 334 | res.writeHead(302, { location: '/foobar' }) 335 | return res.end() 336 | } 337 | // one comes in here 338 | t.equal(req.url, '/foobar', 'got /foobar') 339 | res.writeHead(200, { 'content-type': 'application/json' }) 340 | res.end(stringify(expectedResponse)) 341 | }) 342 | 343 | server.listen(() => { 344 | const port = server.address().port 345 | const done = after(2, () => { server.close() }) 346 | 347 | jsonist.get('http://localhost:' + port, (err, data) => { 348 | // don't follow redirect, don't get data 349 | t.error(err, 'no error') 350 | t.equal(data, null, 'no redirect, no data') 351 | done() 352 | }) 353 | 354 | jsonist.get('http://localhost:' + port, { followRedirects: true }, (err, data) => { 355 | t.error(err, 'no error') 356 | t.deepEqual(data, expectedResponse, 'redirect, got data') 357 | done() 358 | }) 359 | }) 360 | }) 361 | 362 | test('follow redirect limit', (t) => { 363 | t.plan(6 + 10 + 5 + 10) 364 | 365 | const expectedResponse = { ok: 'foobar!' } 366 | const server = http.createServer((req, res) => { 367 | const m = +req.url.match(/^\/(\d+)/)[1] 368 | if (m < 20) { // 2 requests come in here 369 | t.ok('got /') 370 | res.writeHead(302, { location: '/' + (m + 1) }) 371 | return res.end() 372 | } 373 | // one comes in here 374 | t.equal(req.url, '/20', 'got /20') 375 | res.writeHead(200, { 'content-type': 'application/json' }) 376 | res.end(stringify(expectedResponse)) 377 | }) 378 | 379 | server.listen(() => { 380 | const port = server.address().port 381 | const done = after(3, () => { server.close() }) 382 | 383 | jsonist.get(`http://localhost:${port}/1`, { followRedirects: true }, (err, data) => { 384 | t.ok(err, 'got error') 385 | t.equal(err.message, 'Response was redirected too many times (10)') 386 | done() 387 | }) 388 | 389 | jsonist.get(`http://localhost:${port}/1`, { followRedirects: 5 }, (err, data) => { 390 | t.ok(err, 'got error') 391 | t.equal(err.message, 'Response was redirected too many times (5)') 392 | done() 393 | }) 394 | 395 | jsonist.get(`http://localhost:${port}/11`, { followRedirects: true }, (err, data) => { 396 | t.error(err, 'no error') 397 | t.deepEqual(data, expectedResponse, 'redirect, got data') 398 | done() 399 | }) 400 | }) 401 | }) 402 | 403 | test('server error, non-JSON', (t) => { 404 | t.plan(7) 405 | 406 | const responseText = 'there was an error' 407 | 408 | testServer(responseText, 501) 409 | .on('ready', (url) => { 410 | jsonist.get(url, (err, data, response) => { 411 | t.ok(err) 412 | t.ok(err instanceof jsonist.HttpError) 413 | t.equal(err.data.toString(), responseText, 'got correct response') 414 | t.equal(err.statusCode, 501, 'got correct statusCode') 415 | }) 416 | }) 417 | .on('request', (req, data) => { 418 | t.equal(req.method, 'GET', 'got GET request') 419 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 420 | }) 421 | .on('close', t.ok.bind(t, true, 'ended')) 422 | }) 423 | 424 | test('server error, with-JSON', (t) => { 425 | t.plan(8) 426 | 427 | const responseDoc = 'there was an error' 428 | 429 | testServer(stringify(responseDoc), 501) 430 | .on('ready', (url) => { 431 | jsonist.get(url, (err, data, response) => { 432 | t.ifError(err) 433 | t.deepEqual(data, responseDoc, 'got correct doc') 434 | t.ok(response, 'got response object') 435 | t.equal(response.statusCode, 501, 'got correct status code') 436 | t.equal( 437 | response && response.headers && response.headers['content-type'] 438 | , 'application/json', 'verified response object by content-type header' 439 | ) 440 | }) 441 | }) 442 | .on('request', (req, data) => { 443 | t.equal(req.method, 'GET', 'got GET request') 444 | t.equal(req.headers.accept, 'application/json', 'got correct accept header') 445 | }) 446 | .on('close', t.ok.bind(t, true, 'ended')) 447 | }) 448 | --------------------------------------------------------------------------------