├── .github └── workflows │ └── mikeals-workflow.yml ├── .gitignore ├── README.md ├── package.json ├── src ├── browser.js ├── core.js └── nodejs.js └── test ├── test-basics.js ├── test-compression.js └── test-errors.js /.github/workflows/mikeals-workflow.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Build, Test and maybe Publish 3 | jobs: 4 | test: 5 | name: Build & Test 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node-version: [12.x, 14.x] 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - name: Cache node_modules 17 | id: cache-modules 18 | uses: actions/cache@v1 19 | with: 20 | path: node_modules 21 | key: ${{ matrix.node-version }}-${{ runner.OS }}-build-${{ hashFiles('package.json') }} 22 | - name: Build 23 | if: steps.cache-modules.outputs.cache-hit != 'true' 24 | run: npm install 25 | - name: Test 26 | run: npm_config_yes=true npx best-test@latest 27 | publish: 28 | name: Publish 29 | needs: test 30 | runs-on: ubuntu-latest 31 | if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' ) 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Cache node_modules 35 | id: cache-modules 36 | uses: actions/cache@v1 37 | with: 38 | path: node_modules 39 | key: 12.x-${{ runner.OS }}-build-${{ hashFiles('package.json') }} 40 | - name: Build 41 | if: steps.cache-modules.outputs.cache-hit != 'true' 42 | run: npm install 43 | - name: Test 44 | run: npm_config_yes=true npx best-test@latest 45 | 46 | - name: Publish 47 | uses: mikeal/merge-release@master 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | build 3 | coverage 4 | package-lock.json 5 | node_modules 6 | .DS_Store 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bent 2 | 3 | ![2377](https://img.shields.io/badge/compiled%20bundle-2k-brightgreen) ![1106](https://img.shields.io/badge/gzipped%20bundle-1k-brightgreen) 4 | 5 | Functional HTTP client for Node.js and Browsers with async/await. 6 | 7 | *Incredibly small browser version built on fetch with no external dependencies or polyfills.* 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | const bent = require('bent') 13 | 14 | const getJSON = bent('json') 15 | const getBuffer = bent('buffer') 16 | 17 | let obj = await getJSON('http://site.com/json.api') 18 | let buffer = await getBuffer('http://site.com/image.png') 19 | ``` 20 | 21 | As you can see, bent is a function that returns an async function. 22 | 23 | Bent takes options which constrain what is accepted by the client. 24 | Any response that falls outside the constraints will generate an error. 25 | 26 | You can provide these options in any order, and Bent will figure out which option is which by inspecting the option's type and content. 27 | ```javascript 28 | const post = bent('http://localhost:3000/', 'POST', 'json', 200); 29 | const response = await post('cars/new', {name: 'bmw', wheels: 4}); 30 | ``` 31 | 32 | If you don't set a response encoding (`'json'`, `'string'` or `'buffer'`) 33 | then the *native* response object will be returned after the statusCode check. 34 | 35 | In Node.js, we also add decoding methods that match the Fetch API (`.json()`, 36 | `.text()` and `.arrayBuffer()`). 37 | 38 | ```javascript 39 | const bent = require('bent') 40 | 41 | const getStream = bent('http://site.com') 42 | 43 | let stream = await getStream('/json.api') 44 | // status code 45 | stream.status // 200 46 | stream.statusCode // 200 47 | // optionally decode 48 | const obj = await stream.json() 49 | // or 50 | const str = await stream.text() 51 | ``` 52 | 53 | The following options are available. 54 | 55 | * **HTTP Method**: `'GET'`, `'PUT'`, or any other ALLCAPS string will be 56 | used to set the HTTP method. Defaults to `'GET'`. 57 | * **Response Format**: Available formats are `'string'`, `'buffer'`, and 58 | `'json'`. By default, the response object/stream will be returned instead 59 | of a decoded response. *Browser returns `ArrayBuffer` instead of `Buffer`.* 60 | * **Status Codes**: Any number will be considered an acceptable status code. 61 | By default, `200` is the only acceptable status code. When any status codes 62 | are provided, `200` must be included explicitly in order to be acceptable. 63 | * **Headers**: An object can be passed to set request headers. 64 | * **Base URL**: Any string that begins with 'https:' or 'http:' is 65 | considered the Base URL. Subsequent queries need only pass the remaining 66 | URL string. 67 | 68 | The returned async function is used for subsequent requests. 69 | 70 | When working with Binary this library uses different types in the browser and Node.js. In Node.js all binary must be done 71 | using the `Buffer` type. In the browser you can use ArrayBuffer or any ArrayBuffer view type (UInt8Array, etc). 72 | 73 | ### `async request(url[, body=null, headers={}])` 74 | 75 | * **url**: Fully qualified URL to the remote resource, or in the case that a 76 | base URL is passed the remaining URL string. 77 | * **body**: Request body. Can be a string, a stream (node.js), a buffer (node.js) (see note below), 78 | an ArrayBuffer (browser), or a JSON object. 79 | * **headers**: An object of any headers you need to set for just this request. 80 | 81 | ```javascript 82 | const bent = require('bent') 83 | 84 | const put = bent('PUT', 201) 85 | await put('http://site.com/upload', Buffer.from('test')) 86 | ``` 87 | 88 | Or 89 | 90 | 91 | ```javascript 92 | const bent = require('bent') 93 | 94 | const put = bent('PUT', 201, 'http://site.com') 95 | await put('/upload', Buffer.from('test')) 96 | ``` 97 | 98 | **NOTE:** If the `body` is passed as an `object`, it will be treated 99 | as JSON, stringified and the `Content-Type` will be set to `application/json` 100 | unless already set. A common requirement is to POST using `form-urlencoded`. 101 | This will require you to set the `Content-Type` header to 102 | `application/x-www-form-urlencoded` and to encode the body yourself, 103 | perhaps using 104 | [form-urlencoded](https://www.npmjs.com/package/form-urlencoded). 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bent", 3 | "version": "0.0.0-dev", 4 | "description": "Functional HTTP client for Node.js w/ async/await.", 5 | "main": "src/nodejs.js", 6 | "browser": "src/browser.js", 7 | "scripts": { 8 | "lint": "standard", 9 | "test:node": "hundreds mocha --timeout=5000 test/test-*.js", 10 | "test:browser": "polendina --cleanup --service-worker --worker test/test-*.js", 11 | "test": "npm run lint && npm run test:node && npm run test:browser", 12 | "coverage": "nyc --reporter=html mocha test/test-*.js && npx http-server coverage" 13 | }, 14 | "keywords": [], 15 | "author": "Mikeal Rogers (http://www.mikealrogers.com)", 16 | "license": "Apache-2.0", 17 | "dependencies": { 18 | "bytesish": "^0.4.1", 19 | "caseless": "~0.12.0", 20 | "is-stream": "^2.0.0" 21 | }, 22 | "devDependencies": { 23 | "hundreds": "0.0.2", 24 | "mocha": "^7.0.1", 25 | "polendina": "1.0.0", 26 | "standard": "^14.3.1", 27 | "tsame": "^2.0.1" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/mikeal/bent.git" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global fetch, btoa, Headers */ 3 | const core = require('./core') 4 | 5 | class StatusError extends Error { 6 | constructor (res, ...params) { 7 | super(...params) 8 | 9 | if (Error.captureStackTrace) { 10 | Error.captureStackTrace(this, StatusError) 11 | } 12 | 13 | this.name = 'StatusError' 14 | this.message = res.statusMessage 15 | this.statusCode = res.status 16 | this.res = res 17 | this.json = res.json.bind(res) 18 | this.text = res.text.bind(res) 19 | this.arrayBuffer = res.arrayBuffer.bind(res) 20 | let buffer 21 | const get = () => { 22 | if (!buffer) buffer = this.arrayBuffer() 23 | return buffer 24 | } 25 | Object.defineProperty(this, 'responseBody', { get }) 26 | // match Node.js headers object 27 | this.headers = {} 28 | for (const [key, value] of res.headers.entries()) { 29 | this.headers[key.toLowerCase()] = value 30 | } 31 | } 32 | } 33 | 34 | const mkrequest = (statusCodes, method, encoding, headers, baseurl) => async (_url, body, _headers = {}) => { 35 | _url = baseurl + (_url || '') 36 | let parsed = new URL(_url) 37 | 38 | if (!headers) headers = {} 39 | if (parsed.username) { 40 | headers.Authorization = 'Basic ' + btoa(parsed.username + ':' + parsed.password) 41 | parsed = new URL(parsed.protocol + '//' + parsed.host + parsed.pathname + parsed.search) 42 | } 43 | if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { 44 | throw new Error(`Unknown protocol, ${parsed.protocol}`) 45 | } 46 | 47 | if (body) { 48 | if (body instanceof ArrayBuffer || 49 | ArrayBuffer.isView(body) || 50 | typeof body === 'string' 51 | ) { 52 | // noop 53 | } else if (typeof body === 'object') { 54 | body = JSON.stringify(body) 55 | headers['Content-Type'] = 'application/json' 56 | } else { 57 | throw new Error('Unknown body type.') 58 | } 59 | } 60 | 61 | _headers = new Headers({ ...(headers || {}), ..._headers }) 62 | 63 | const resp = await fetch(parsed, { method, headers: _headers, body }) 64 | resp.statusCode = resp.status 65 | 66 | if (!statusCodes.has(resp.status)) { 67 | throw new StatusError(resp) 68 | } 69 | 70 | if (encoding === 'json') return resp.json() 71 | else if (encoding === 'buffer') return resp.arrayBuffer() 72 | else if (encoding === 'string') return resp.text() 73 | else return resp 74 | } 75 | 76 | module.exports = core(mkrequest) 77 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const encodings = new Set(['json', 'buffer', 'string']) 3 | 4 | module.exports = mkrequest => (...args) => { 5 | const statusCodes = new Set() 6 | let method 7 | let encoding 8 | let headers 9 | let baseurl = '' 10 | 11 | args.forEach(arg => { 12 | if (typeof arg === 'string') { 13 | if (arg.toUpperCase() === arg) { 14 | if (method) { 15 | const msg = `Can't set method to ${arg}, already set to ${method}.` 16 | throw new Error(msg) 17 | } else { 18 | method = arg 19 | } 20 | } else if (arg.startsWith('http:') || arg.startsWith('https:')) { 21 | baseurl = arg 22 | } else { 23 | if (encodings.has(arg)) { 24 | encoding = arg 25 | } else { 26 | throw new Error(`Unknown encoding, ${arg}`) 27 | } 28 | } 29 | } else if (typeof arg === 'number') { 30 | statusCodes.add(arg) 31 | } else if (typeof arg === 'object') { 32 | if (Array.isArray(arg) || arg instanceof Set) { 33 | arg.forEach(code => statusCodes.add(code)) 34 | } else { 35 | if (headers) { 36 | throw new Error('Cannot set headers twice.') 37 | } 38 | headers = arg 39 | } 40 | } else { 41 | throw new Error(`Unknown type: ${typeof arg}`) 42 | } 43 | }) 44 | 45 | if (!method) method = 'GET' 46 | if (statusCodes.size === 0) { 47 | statusCodes.add(200) 48 | } 49 | 50 | return mkrequest(statusCodes, method, encoding, headers, baseurl) 51 | } 52 | -------------------------------------------------------------------------------- /src/nodejs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const http = require('http') 3 | const https = require('https') 4 | const { URL } = require('url') 5 | const isStream = require('is-stream') 6 | const caseless = require('caseless') 7 | const bytes = require('bytesish') 8 | const bent = require('./core') 9 | const zlib = require('zlib') 10 | const { PassThrough } = require('stream') 11 | 12 | const compression = {} 13 | 14 | /* istanbul ignore else */ 15 | if (zlib.createBrotliDecompress) compression.br = () => zlib.createBrotliDecompress() 16 | /* istanbul ignore else */ 17 | if (zlib.createGunzip) compression.gzip = () => zlib.createGunzip() 18 | /* istanbul ignore else */ 19 | if (zlib.createInflate) compression.deflate = () => zlib.createInflate() 20 | 21 | const acceptEncoding = Object.keys(compression).join(', ') 22 | 23 | const getResponse = resp => { 24 | const ret = new PassThrough() 25 | ret.statusCode = resp.statusCode 26 | ret.status = resp.statusCode 27 | ret.statusMessage = resp.statusMessage 28 | ret.headers = resp.headers 29 | ret._response = resp 30 | if (ret.headers['content-encoding']) { 31 | const encodings = ret.headers['content-encoding'].split(', ').reverse() 32 | while (encodings.length) { 33 | const enc = encodings.shift() 34 | if (compression[enc]) { 35 | const decompress = compression[enc]() 36 | decompress.on('error', (e) => ret.emit('error', new Error('ZBufError', e))) 37 | resp = resp.pipe(decompress) 38 | } else { 39 | break 40 | } 41 | } 42 | } 43 | return resp.pipe(ret) 44 | } 45 | 46 | class StatusError extends Error { 47 | constructor (res, ...params) { 48 | super(...params) 49 | 50 | Error.captureStackTrace(this, StatusError) 51 | this.name = 'StatusError' 52 | this.message = res.statusMessage 53 | this.statusCode = res.statusCode 54 | this.json = res.json 55 | this.text = res.text 56 | this.arrayBuffer = res.arrayBuffer 57 | this.headers = res.headers 58 | let buffer 59 | const get = () => { 60 | if (!buffer) buffer = this.arrayBuffer() 61 | return buffer 62 | } 63 | Object.defineProperty(this, 'responseBody', { get }) 64 | } 65 | } 66 | 67 | const getBuffer = stream => new Promise((resolve, reject) => { 68 | const parts = [] 69 | stream.on('error', reject) 70 | stream.on('end', () => resolve(Buffer.concat(parts))) 71 | stream.on('data', d => parts.push(d)) 72 | }) 73 | 74 | const decodings = res => { 75 | let _buffer 76 | res.arrayBuffer = () => { 77 | if (!_buffer) { 78 | _buffer = getBuffer(res) 79 | return _buffer 80 | } else { 81 | throw new Error('body stream is locked') 82 | } 83 | } 84 | res.text = () => res.arrayBuffer().then(buff => buff.toString()) 85 | res.json = async () => { 86 | const str = await res.text() 87 | try { 88 | return JSON.parse(str) 89 | } catch (e) { 90 | e.message += `str"${str}"` 91 | throw e 92 | } 93 | } 94 | } 95 | 96 | const mkrequest = (statusCodes, method, encoding, headers, baseurl) => (_url, body = null, _headers = {}) => { 97 | _url = baseurl + (_url || '') 98 | const parsed = new URL(_url) 99 | let h 100 | if (parsed.protocol === 'https:') { 101 | h = https 102 | } else if (parsed.protocol === 'http:') { 103 | h = http 104 | } else { 105 | throw new Error(`Unknown protocol, ${parsed.protocol}`) 106 | } 107 | const request = { 108 | path: parsed.pathname + parsed.search, 109 | port: parsed.port, 110 | method: method, 111 | headers: { ...(headers || {}), ..._headers }, 112 | hostname: parsed.hostname 113 | } 114 | if (parsed.username || parsed.password) { 115 | request.auth = [parsed.username, parsed.password].join(':') 116 | } 117 | const c = caseless(request.headers) 118 | if (encoding === 'json') { 119 | if (!c.get('accept')) { 120 | c.set('accept', 'application/json') 121 | } 122 | } 123 | if (!c.has('accept-encoding')) { 124 | c.set('accept-encoding', acceptEncoding) 125 | } 126 | return new Promise((resolve, reject) => { 127 | const req = h.request(request, async res => { 128 | res = getResponse(res) 129 | res.on('error', reject) 130 | decodings(res) 131 | res.status = res.statusCode 132 | if (!statusCodes.has(res.statusCode)) { 133 | return reject(new StatusError(res)) 134 | } 135 | 136 | if (!encoding) return resolve(res) 137 | else { 138 | /* istanbul ignore else */ 139 | if (encoding === 'buffer') { 140 | resolve(res.arrayBuffer()) 141 | } else if (encoding === 'json') { 142 | resolve(res.json()) 143 | } else if (encoding === 'string') { 144 | resolve(res.text()) 145 | } 146 | } 147 | }) 148 | req.on('error', reject) 149 | if (body) { 150 | if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) { 151 | body = bytes.native(body) 152 | } 153 | if (Buffer.isBuffer(body)) { 154 | // noop 155 | } else if (typeof body === 'string') { 156 | body = Buffer.from(body) 157 | } else if (isStream(body)) { 158 | body.pipe(req) 159 | body = null 160 | } else if (typeof body === 'object') { 161 | if (!c.has('content-type')) { 162 | req.setHeader('content-type', 'application/json') 163 | } 164 | body = Buffer.from(JSON.stringify(body)) 165 | } else { 166 | reject(new Error('Unknown body type.')) 167 | } 168 | if (body) { 169 | req.setHeader('content-length', body.length) 170 | req.end(body) 171 | } 172 | } else { 173 | req.end() 174 | } 175 | }) 176 | } 177 | 178 | module.exports = bent(mkrequest) 179 | -------------------------------------------------------------------------------- /test/test-basics.js: -------------------------------------------------------------------------------- 1 | /* globals atob, it */ 2 | 'use strict' 3 | const bent = require('../') 4 | const assert = require('assert') 5 | const tsame = require('tsame') 6 | const { PassThrough } = require('stream') 7 | 8 | const http = require('http') 9 | 10 | const test = it 11 | 12 | const same = (x, y) => assert.ok(tsame(x, y)) 13 | 14 | const baseurl = 'https://echo-server.mikeal.now.sh/src' 15 | const u = path => baseurl + path 16 | 17 | const enc = str => (new TextEncoder()).encode(str).buffer 18 | const dec = str => Uint8Array.from(atob(str), c => c.charCodeAt(0)).buffer 19 | const decode = arr => (new TextDecoder('utf-8')).decode(arr) 20 | 21 | test('basic 200 ok', async () => { 22 | const request = bent('string') 23 | const str = await request(u('/echo.js?body=ok')) 24 | same(str, 'ok') 25 | }) 26 | 27 | test('basic 200 ok baseurl', async () => { 28 | const request = bent('string', baseurl) 29 | const str = await request('/echo.js?body=ok') 30 | same(str, 'ok') 31 | }) 32 | 33 | test('basic 200', async () => { 34 | const request = bent() 35 | const res = await request(u('/echo.js?body=ok')) 36 | same(res.statusCode, 200) 37 | }) 38 | 39 | test('basic buffer', async () => { 40 | const request = bent('buffer') 41 | const buff = await request(u('/echo.js?body=ok')) 42 | if (buff instanceof ArrayBuffer) { 43 | same(buff, enc('ok')) 44 | } else { 45 | same(buff, Buffer.from('ok')) 46 | } 47 | }) 48 | 49 | test('double buffer decode', async () => { 50 | const request = bent() 51 | const resp = await request(u('/echo.js?body=ok')) 52 | const validate = buff => { 53 | if (buff instanceof ArrayBuffer) { 54 | same(buff, enc('ok')) 55 | } else { 56 | same(buff, Buffer.from('ok')) 57 | } 58 | } 59 | validate(await resp.arrayBuffer()) 60 | let threw = true 61 | try { 62 | await resp.arrayBuffer() 63 | threw = false 64 | } catch (e) { 65 | if (!e.message.includes('body stream is locked')) throw e 66 | } 67 | assert.ok(threw) 68 | }) 69 | 70 | test('basic json', async () => { 71 | const request = bent('json') 72 | const json = await request(u('/info.js')) 73 | same(json.method, 'GET') 74 | }) 75 | 76 | test('json based media type', async () => { 77 | const request = bent('json', { accept: 'application/vnd.something.com' }) 78 | const json = await request(u('/info.js')) 79 | same(json.headers.accept, 'application/vnd.something.com') 80 | }) 81 | 82 | test('basic PUT', async () => { 83 | const request = bent('PUT', 'json') 84 | let body 85 | if (process.browser) { 86 | body = enc(Math.random().toString()) 87 | } else { 88 | body = Buffer.from(Math.random().toString()) 89 | } 90 | const json = await request(u('/info.js'), body) 91 | if (process.browser) { 92 | same(dec(json.base64), body) 93 | } else { 94 | same(Buffer.from(json.base64, 'base64'), body) 95 | } 96 | }) 97 | 98 | test('base PUT string', async () => { 99 | const request = bent('PUT', 'json') 100 | const json = await request(u('/info.js'), 'teststring') 101 | if (process.browser) { 102 | same(atob(json.base64), 'teststring') 103 | } else { 104 | same(Buffer.from(json.base64, 'base64').toString(), 'teststring') 105 | } 106 | }) 107 | 108 | test('status 201', async () => { 109 | const request = bent('string', 201) 110 | const str = await request(u('/echo.js?statusCode=201&body=ok')) 111 | same(str, 'ok') 112 | 113 | try { 114 | await request(u('/echo.js?body=ok')) 115 | throw new Error('Call should have thrown.') 116 | } catch (e) { 117 | same(e.message, process.browser ? null : 'OK') 118 | // basic header test 119 | same(e.headers['content-length'], '2') 120 | } 121 | }) 122 | 123 | test('multiple status', async () => { 124 | const request = bent('string', [200, 201]) 125 | const str200 = await request(u('/echo.js?body=ok')) 126 | same(str200, 'ok') 127 | 128 | const str201 = await request(u('/echo.js?statusCode=201&body=ok')) 129 | same(str201, 'ok') 130 | 131 | try { 132 | await request(u('/echo.js?statusCode=202&body=ok')) 133 | throw new Error('Call should have thrown.') 134 | } catch (e) { 135 | same(e.message, process.browser ? null : 'Accepted') 136 | // basic header test 137 | same(e.headers['content-length'], '2') 138 | } 139 | }) 140 | 141 | test('PUT stream', async () => { 142 | const body = Buffer.from(Math.random().toString()) 143 | const request = bent('PUT', 'json') 144 | const b = new PassThrough() 145 | const res = request(u('/info.js'), b) 146 | b.end(body) 147 | const info = await res 148 | same(info.method, 'PUT') 149 | // Unfortunately, we can't test this against lamda cause it doesn't support 150 | // transfer-encoding: chunked. 151 | // t.same(Buffer.from(info.base64, 'base64'), body) 152 | }) 153 | 154 | test('PUT JSON', async () => { 155 | const request = bent('PUT', 'json') 156 | const info = await request(u('/info.js'), { ok: 200 }) 157 | let res 158 | if (process.browser) { 159 | res = JSON.parse(atob(info.base64)) 160 | } else { 161 | res = JSON.parse(Buffer.from(info.base64, 'base64').toString()) 162 | } 163 | same(res, { ok: 200 }) 164 | same(info.headers['content-type'], 'application/json') 165 | }) 166 | 167 | if (process.browser) { 168 | test('500 Response body and message', async () => { 169 | const request = bent() 170 | let body 171 | let _e 172 | try { 173 | await request(u('/echo.js?statusCode=500&body=ok')) 174 | } catch (e) { 175 | _e = e 176 | body = e.responseBody 177 | } 178 | const validate = buffer => { 179 | if (process.browser) { 180 | same(decode(buffer), 'ok') 181 | } else { 182 | same(buffer.toString(), 'ok') 183 | } 184 | } 185 | validate(await body) 186 | // should be able to access again 187 | validate(await _e.responseBody) 188 | 189 | same(_e.message, null) 190 | }) 191 | } else { 192 | test('500 Response body and message', async () => { 193 | const request = bent() 194 | let body 195 | let _e 196 | try { 197 | await request(u('/echo.js?statusCode=500&body=ok')) 198 | } catch (e) { 199 | _e = e 200 | body = e.responseBody 201 | } 202 | const validate = buffer => { 203 | if (process.browser) { 204 | same(decode(buffer), 'ok') 205 | } else { 206 | same(buffer.toString(), 'ok') 207 | } 208 | } 209 | validate(await body) 210 | // should be able to access again 211 | validate(await _e.responseBody) 212 | 213 | same(_e.message, 'Internal Server Error') 214 | }) 215 | } 216 | 217 | test('auth', async () => { 218 | const request = bent('https://test:pass@httpbin.org/basic-auth/test/pass', 'json') 219 | const obj = await request() 220 | same(obj, { authenticated: true, user: 'test' }) 221 | }) 222 | 223 | if (process.browser) { 224 | test('override headers', async () => { 225 | const request = bent('string', { Accept: 'application/json' }) 226 | let info = await request(u('/info.js'), null, { Accept: 'application/xml' }) 227 | info = JSON.parse(info) 228 | same(info.headers.accept, 'application/xml') 229 | }) 230 | } else { 231 | test('override headers', async () => { 232 | const request = bent('json', { 'X-Default': 'ok', 'X-Override-Me': 'not overriden' }) 233 | const info = await request(u('/info.js'), null, { 'X-Override-Me': 'overriden', 'X-New': 'ok' }) 234 | same(info.headers['x-default'], 'ok') 235 | same(info.headers['x-override-me'], 'overriden') 236 | same(info.headers['x-new'], 'ok') 237 | }) 238 | 239 | test('manually-set content-type header when body is present', async () => { 240 | const server = http.createServer((request, response) => { 241 | response.statusCode = request.headers['content-type'] === 'application/jose+json' ? 200 : 400 242 | response.end() 243 | }) 244 | await new Promise((resolve, reject) => { 245 | server.listen(9999, () => { 246 | resolve() 247 | }) 248 | }) 249 | const request = bent('POST') 250 | const response = request('http://localhost:9999', { ok: true }, { 'content-type': 'application/jose+json' }) 251 | const info = await response 252 | same(info.statusCode, 200) 253 | server.close() 254 | }) 255 | } 256 | -------------------------------------------------------------------------------- /test/test-compression.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* globals it */ 3 | const bent = require('../') 4 | const assert = require('assert') 5 | const tsame = require('tsame') 6 | const qs = require('querystring') 7 | const zlib = require('zlib') 8 | 9 | const test = it 10 | 11 | const same = (x, y) => assert.ok(tsame(x, y)) 12 | 13 | const baseurl = 'https://echo-server.mikeal.now.sh/src' 14 | const u = path => baseurl + path 15 | 16 | if (!process.browser) { 17 | test('accept header', async () => { 18 | let request = bent('json') 19 | let json = await request(u('/info.js')) 20 | same(json.headers['accept-encoding'], 'br, gzip, deflate') 21 | 22 | request = bent('json', { 'Accept-Encoding': 'test' }) 23 | json = await request(u('/info.js')) 24 | same(json.headers['accept-encoding'], 'test') 25 | }) 26 | 27 | test('brotli', async () => { 28 | const request = bent('string', baseurl) 29 | const base64 = zlib.brotliCompressSync('ok').toString('base64') 30 | const headers = 'content-encoding:br' 31 | const str = await request(`/echo.js?${qs.stringify({ base64, headers })}`) 32 | same(str, 'ok') 33 | }) 34 | 35 | test('gzip', async () => { 36 | const request = bent('string', baseurl) 37 | const base64 = zlib.gzipSync('ok').toString('base64') 38 | const headers = 'content-encoding:gzip' 39 | const str = await request(`/echo.js?${qs.stringify({ base64, headers })}`) 40 | same(str, 'ok') 41 | }) 42 | 43 | test('deflate', async () => { 44 | const request = bent('string', baseurl) 45 | const base64 = zlib.deflateSync('ok').toString('base64') 46 | const headers = 'content-encoding:deflate' 47 | const str = await request(`/echo.js?${qs.stringify({ base64, headers })}`) 48 | same(str, 'ok') 49 | }) 50 | 51 | test('unknown', async () => { 52 | const request = bent('buffer', baseurl) 53 | const base64 = zlib.gzipSync('untouched').toString('base64') 54 | const headers = 'content-encoding:myown' 55 | const buffer = await request(`/echo.js?${qs.stringify({ base64, headers })}`) 56 | const str = zlib.gunzipSync(buffer).toString('utf8') 57 | same(str, 'untouched') 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /test/test-errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* globals it */ 3 | const bent = require('../') 4 | const tsame = require('tsame') 5 | const assert = require('assert') 6 | const zlib = require('zlib') 7 | const ttype = (e, str) => same(e.constructor.name, str) 8 | const qs = require('querystring') 9 | 10 | const test = it 11 | const same = (x, y) => assert.ok(tsame(x, y)) 12 | 13 | test('Invalid encoding', done => { 14 | try { 15 | bent('blah') 16 | } catch (e) { 17 | ttype(e, 'Error') 18 | same(e.message, 'Unknown encoding, blah') 19 | done() 20 | } 21 | }) 22 | 23 | test('double method', done => { 24 | try { 25 | bent('GET', 'PUT') 26 | } catch (e) { 27 | ttype(e, 'Error') 28 | same(e.message, 'Can\'t set method to PUT, already set to GET.') 29 | done() 30 | } 31 | }) 32 | 33 | test('double headers', done => { 34 | try { 35 | bent({}, {}) 36 | } catch (e) { 37 | ttype(e, 'Error') 38 | same(e.message, 'Cannot set headers twice.') 39 | done() 40 | } 41 | }) 42 | 43 | test('unknown protocol', async () => { 44 | try { 45 | const request = bent() 46 | await request('ftp://host.com') 47 | throw new Error('Should have already failed') 48 | } catch (e) { 49 | ttype(e, 'Error') 50 | same(e.message, 'Unknown protocol, ftp:') 51 | } 52 | }) 53 | 54 | test('Invalid type', done => { 55 | try { 56 | bent(true) 57 | } catch (e) { 58 | ttype(e, 'Error') 59 | same(e.message, 'Unknown type: boolean') 60 | done() 61 | } 62 | }) 63 | 64 | test('Invalid body', async () => { 65 | const r = bent('PUT') 66 | try { 67 | await r('http://localhost:3000', true) 68 | throw new Error('Should have failed') 69 | } catch (e) { 70 | ttype(e, 'Error') 71 | same(e.message, 'Unknown body type.') 72 | } 73 | }) 74 | 75 | test('Invalid json', async () => { 76 | const r = bent('GET', 'json') 77 | try { 78 | await r('https://echo-server.mikeal.now.sh/src/echo.js?body=[asdf]') 79 | throw new Error('Should have failed') 80 | } catch (e) { 81 | assert.ok(e.message.startsWith('Unexpected token a in JSON')) 82 | } 83 | }) 84 | 85 | const getError = async () => { 86 | const r = bent(201) 87 | try { 88 | await r('https://echo-server.mikeal.now.sh/src/echo.js?body="asdf"') 89 | throw new Error('Should have failed') 90 | } catch (e) { 91 | ttype(e, 'StatusError') 92 | return e 93 | } 94 | } 95 | 96 | test('error decodings', async () => { 97 | let e = await getError() 98 | same(await e.text(), '"asdf"') 99 | e = await getError() 100 | same(await e.json(), 'asdf') 101 | }) 102 | 103 | if (!process.browser) { 104 | test('Z_BUF_ERROR error', async () => { 105 | const request = bent('json') 106 | try { 107 | await request('https://echo-server.mikeal.now.sh/src/echo.js?headers=content-encoding%3Agzip%2Ccontent-type%3Aapplication%2Fjson') 108 | } catch (e) { 109 | ttype(e, 'Error') 110 | return e 111 | } 112 | }) 113 | test('gzip json compresssion SyntaxError', async () => { 114 | const request = bent('json') 115 | const base64 = zlib.gzipSync('ok').toString('base64') 116 | const headers = 'content-encoding:gzip,content-type:application/json' 117 | try { 118 | await request(`https://echo-server.mikeal.now.sh/src/echo.js?${qs.stringify({ base64, headers })}`) 119 | } catch (e) { 120 | ttype(e, 'SyntaxError') 121 | return e 122 | } 123 | }) 124 | } 125 | --------------------------------------------------------------------------------