Repository: brianleroux/tiny-json-http Branch: main Commit: 1cbfddbe4c62 Files: 14 Total size: 22.3 KB Directory structure: gitextract_t8ymbo8n/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── _read.js ├── _write.js ├── index.js ├── package.json ├── readme.md ├── test-form-urlencoded.js ├── test-get.js ├── test-post.js ├── test-promise.js └── test-qq-multipart.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Node CI # Push tests commits; pull_request tests PR merges on: [ push, pull_request ] defaults: run: shell: bash jobs: # Test the build build: # Setup runs-on: ${{ matrix.os }} strategy: matrix: node-version: [ 12.x, 14.x, 16.x, 18.x ] os: [ windows-latest, ubuntu-latest, macOS-latest ] # Go steps: - name: Check out repo uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Env run: | echo "Event name: ${{ github.event_name }}" echo "Git ref: ${{ github.ref }}" echo "GH actor: ${{ github.actor }}" echo "SHA: ${{ github.sha }}" VER=`node --version`; echo "Node ver: $VER" VER=`npm --version`; echo "npm ver: $VER" - name: Install run: npm install - name: Build run: npm run build - name: Test run: npm test env: CI: true # Publish to package registries publish: # Setup needs: build if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest # Go steps: - name: Check out repo uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: lts/* registry-url: https://registry.npmjs.org/ - name: Install run: npm install - name: Build run: npm run build - name: Publish @latest to npm if: contains(github.ref, 'RC') == false #'!contains()'' doesn't work lol run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .gitignore ================================================ bundle.js dist.js node_modules/ package-lock.json ================================================ FILE: .npmignore ================================================ .* test-*.js index.js _read.js _write.js node_modules ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: _read.js ================================================ var http = require('http') var https = require('https') var url = require('url') var qs = require('querystring') module.exports = function _read(httpMethod, options, callback) { // deep copy options options = JSON.parse(JSON.stringify(options)) // alias body = data if (options.body && !options.data) { options.data = options.body } // require options.url or fail noisily if (!options.url) { throw Error('options.url required') } // setup promise if there is no callback var promise if (!callback) { promise = new Promise(function(res, rej) { callback = function(err, result) { err ? rej(err) : res(result) } }) } // parse out the options from options.url var opts = url.parse(options.url) var method = opts.protocol === 'https:' ? https.request : http.request var defaultContentType = 'application/json; charset=utf-8' // check for additional query params if (options.data) { var isSearch = !!opts.search options.url += (isSearch? '&' : '?') + qs.stringify(options.data) opts = url.parse(options.url) } // add timeout if it exists if (options.timeout) { opts.timeout = options.timeout } // wrangle defaults opts.method = httpMethod opts.headers = options.headers || {} opts.headers['user-agent'] = opts.headers['user-agent'] || opts.headers['User-Agent'] || 'tiny-http' opts.headers['content-type'] = opts.headers['content-type'] || opts.headers['Content-Type'] || defaultContentType // make a request var req = method(opts, function _res(res) { var raw = [] // keep our buffers here var ok = res.statusCode >= 200 && res.statusCode < 303 res.on('data', function _data(chunk) { raw.push(chunk) }) res.on('end', function _end() { var err = null var result = null var isJSON = res.headers['content-type'] && (res.headers['content-type'].startsWith('application/json') || res.headers['content-type'].match(/^application\/.*json/)) try { result = Buffer.concat(raw) if (!options.buffer) { var strRes = result.toString() result = strRes && isJSON ? JSON.parse(strRes) : strRes } } catch(e) { err = e } if (!ok) { err = Error('GET failed with: ' + res.statusCode) err.raw = res err.body = isJSON? JSON.stringify(result) : result.toString() err.statusCode = res.statusCode callback(err) } else { callback(err, {body:result, headers:res.headers}) } }) }) req.on('error', callback) req.end() return promise } ================================================ FILE: _write.js ================================================ var qs = require('querystring') var http = require('http') var https = require('https') var FormData = require('@brianleroux/form-data') var url = require('url') module.exports = function _write(httpMethod, options, callback) { // deep copy options if no buffers being passed in let formopts = options.data || options.body let notplain = k => typeof formopts[k] != 'string' let basic = formopts && Object.keys(formopts).some(notplain) === false if (basic) { options = JSON.parse(JSON.stringify(options)) } // alias body = data if (options.body && !options.data) { options.data = options.body } // require options.url or fail noisily if (!options.url) { throw Error('options.url required') } // setup promise if there is no callback var promise if (!callback) { promise = new Promise(function(res, rej) { callback = function(err, result) { err ? rej(err) : res(result) } }) } // parse out the options from options.url var opts = url.parse(options.url) var method = opts.protocol === 'https:'? https.request : http.request var defaultContentType = 'application/json; charset=utf-8' // add timeout if (options.timeout) { opts.timeout = timeout } // wrangle defaults opts.method = httpMethod opts.headers = options.headers || {} opts.headers['user-agent'] = opts.headers['user-agent'] || opts.headers['User-Agent'] || 'tiny-http' opts.headers['content-type'] = opts.headers['content-type'] || opts.headers['Content-Type'] || defaultContentType // default to regular POST body (url enc) var postData = qs.stringify(options.data || {}) function is(headers, type) { var regex = type instanceof RegExp var upper = headers['Content-Type'] var lower = headers['content-type'] var isU = upper && (regex ? upper.match(type) : upper.startsWith(type)) var isL = lower && (regex ? lower.match(type) : lower.startsWith(type)) return isU || isL } // if we're posting JSON stringify options.data var isJSON = is(opts.headers, /^application\/.*json/) if (isJSON) { postData = JSON.stringify(options.data || {}) } // if we're doing a application/x-www-form-urlencoded to upload files // we'll overload `method` and use the custom form-data submit instead of http.request var isUrlEncoded = is(opts.headers, 'application/x-www-form-urlencoded') if (isUrlEncoded) { postData = Object.keys(options.data) .map(k => `${encodeURI(k)}=${encodeURI(options.data[k])}`) .join('&') } // ensure we know the len ~after~ we set the postData opts.headers['Content-Length'] = Buffer.byteLength(postData) // if we're doing a mutipart/form-data to upload files // we'll overload `method` and use the custom form-data submit instead of http.request var isMultipart = is(opts.headers, 'multipart/form-data') if (isMultipart) { method = function _multiPartFormDataPost(params, streamback) { var form = new FormData Object.keys(options.data).forEach(k=> { form.append(k, options.data[k]) }) // remove stuff generated by form.submit delete opts.headers['Content-Type'] delete opts.headers['content-type'] delete opts.headers['Content-Length'] delete opts.headers['content-length'] // perform a multipart/form-data POST request form.submit(opts, function _submit(err, res) { if (err) callback(err) else streamback(res) }) } } // make a request var req = method(opts, function(res) { var raw = [] // keep our buffers here var ok = res.statusCode >= 200 && res.statusCode < 303 res.on('data', function _data(chunk) { raw.push(chunk) }) res.on('end', function _end() { var err = null var result = null try { result = Buffer.concat(raw) if (!options.buffer) { var isJSON = is(res.headers, /^application\/.*json/) var strRes = result.toString() result = strRes && isJSON ? JSON.parse(strRes) : strRes } } catch (e) { err = e } if (!ok) { err = Error(httpMethod + ' failed with: ' + res.statusCode) err.raw = res err.body = result err.statusCode = res.statusCode callback(err) } else { callback(err, {body:result, headers:res.headers}) } }) }) if (!isMultipart) { req.on('error', callback) req.write(postData) req.end() } return promise } ================================================ FILE: index.js ================================================ var _read = require('./_read') var _write = require('./_write') module.exports = { get: _read.bind({}, 'GET'), head: _read.bind({}, 'HEAD'), options: _read.bind({}, 'OPTIONS'), post: _write.bind({}, 'POST'), put: _write.bind({}, 'PUT'), patch: _write.bind({}, 'PATCH'), del: _write.bind({}, 'DELETE'), delete: _write.bind({}, 'DELETE'), } ================================================ FILE: package.json ================================================ { "name": "tiny-json-http", "version": "7.5.1", "main": "dist.js", "scripts": { "test": "npm run build && tape test-* | tap-spec", "build": "esbuild index.js --bundle --outfile=dist.js --platform=node" }, "repository": { "type": "git", "url": "git+https://github.com/brianleroux/tiny-json-http.git" }, "author": "Brian LeRoux ", "license": "Apache-2.0", "devDependencies": { "@brianleroux/form-data": "^1.0.3", "body-parser": "^1.20.2", "esbuild": "^0.17.10", "express": "^4.18.2", "tap-spec": "^5.0.0", "tape": "^5.6.3", "uglify-es": "^3.3.7" } } ================================================ FILE: readme.md ================================================ # tiny-json-http Minimalist `HTTP` client for `GET`, `POST`, `PUT`, `PATCH` and `DELETE` `JSON` payloads - Zero dependencies: perfect for AWS Lambda - Sensible default: assumes buffered JSON responses - System symmetry: Node style errback API, or Promises for use with Async/Await ```bash npm i tiny-json-http --save ``` ### API #### Read methods - `tiny.get(options[, callback])` - `tiny.head(options[, callback])` - `tiny.options(options[, callback])` #### Write methods - `tiny.post(options[, callback])` - `tiny.put(options[, callback])` - `tiny.patch(options[, callback])` - `tiny.del(options[, callback)]` _*callback is optional, tiny methods will return a promise if no callback is provided_ ### Options - `url` *required* - `data` form vars for `tiny.post`, `tiny.put`, `tiny.patch`, and `tiny.delete` otherwise querystring vars for `tiny.get` - `headers` key/value map used for headers (including support for uploading files with `multipart/form-data`) - `buffer` if set to `true` the response body is returned as a buffer ### Callback values - `err` a real javascript `Error` if there was one - `data` an object with `headers` and `body` keys ### Promises - if no `callback` is provided to the tiny-json-http methods, a promise is returned - perfect for use of async/await ## Examples #### With Async / Await ```javascript var tiny = require('tiny-json-http') var url = 'http://www.randomkittengenerator.com' ;(async function _iife() { try { console.log(await tiny.get({url})) } catch (err) { console.log('ruh roh!', err) } })(); ``` #### With Callback ```javascript var tiny = require('tiny-json-http') var url = 'http://www.randomkittengenerator.com' tiny.get({url}, function _get(err, result) { if (err) { console.log('ruh roh!', err) } else { console.log(result) } }) ``` Check out the tests for more examples! :heart_decoration: ================================================ FILE: test-form-urlencoded.js ================================================ var test = require('tape') var tiny = require('./dist.js') var http = require('http') var server let body = '' test('startup', t=> { t.plan(1) server = http.createServer((req, res) => { req.on('data', chunk => body += chunk) req.on('end', () => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ body: body.concat(), gotPost:true, ok:true })) }) }) server.listen(3000) t.pass('started server') }) test('supports form-urlencoded bodies', t=> { t.plan(3) var url = 'http://localhost:3000' var data = { foo: 'bar', this: 'is form url encoded!' } var headers = { 'content-type': 'application/x-www-form-urlencoded' } tiny.post({url, data, headers}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.ok(result.body.gotPost, 'got a post') t.equal(result.body.body, 'foo=bar&this=is%20form%20url%20encoded!', 'got form-urlencoded response') console.log(result) } }) }) test('shutdown', t=> { t.plan(1) server.close() t.ok(true, 'closed server') }) ================================================ FILE: test-get.js ================================================ var test = require('tape') var express = require('express') var bodyParser = require('body-parser') var app = express() var tiny = require('./dist.js') var server app.use(bodyParser.json()) app.use(bodyParser.urlencoded({extended:true})) app.get('/json', (req, res)=> { res.json({hello: 'there'}) }) app.get('/void', (req, res)=> { res.json('') }) app.options('/opts', (req, res) => { res.setHeader('allow', 'OPTIONS, GET') res.json('') }) app.get('/goaway', (req, res) => { res.setHeader('location', '/') res.statusCode = 302 res.send() }) test('startup', t=> { t.plan(1) server = app.listen(3001, x=> { t.ok(true, 'started server') }) }) test('can get a url', t=> { t.plan(3) var url = 'https://brian.io' tiny.get({url}, function __got(err, result) { if (err) { t.fail(err.statusCode, 'failed to get') } else { t.ok(result, 'got a result') t.ok(result.headers, 'got headers') t.ok(result.body, 'got body') console.log(result) } }) }) test('can get json', t=> { t.plan(2) var url = 'http://localhost:3001/json' tiny.get({url}, function __json(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.equal(result.body.hello, 'there', 'body is an object') console.log(err, result) } }) }) test('can get and handle "no content"', t=> { t.plan(2) var url = 'http://localhost:3001/void' tiny.get({url}, function __void(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result (empty tho)') t.is(result.body, '') console.log(result) } }) }) test('get fails gracefully', t=> { t.plan(1) var url = 'http://nop333.ca' tiny.get({url}, function __ruhroh(err, result) { if (err) { t.ok(err, 'got err as expected') console.log(err) } else { t.fail(result, 'should not succeed') } }) }) test('can head a url', t=> { t.plan(2) var url = 'https://www.google.com' tiny.head({url}, function __got(err, result) { if (err) { t.fail(err.statusCode, 'failed to head') } else { t.ok(result, 'got a result') t.ok(result.headers, 'got headers') console.log(JSON.stringify(result, null, 2).substring(0,75), '...') } }) }) test('can options a url', t=> { t.plan(2) var url = 'http://localhost:3001/opts' tiny.options({url}, function __got(err, result) { if (err) { t.fail(err.statusCode, 'failed to options') } else { t.ok(result, 'got a result') t.ok(result.headers.allow, 'got headers') console.log(result) } }) }) test('can handles redirect', t=> { t.plan(2) var url = 'http://localhost:3001/goaway' tiny.get({url}, function __got(err, result) { if (err) { t.fail(err.statusCode, 'failed to handle redirect') } else { t.ok(result, 'got a result') t.ok(result.headers.location, 'got a location header') console.log(result) } }) }) test('shutdown', t=> { t.plan(1) server.close() t.ok(true, 'closed server') }) ================================================ FILE: test-post.js ================================================ var test = require('tape') var express = require('express') var bodyParser = require('body-parser') var app = express() var tiny = require('./dist.js') var server app.use(bodyParser.json()) app.use(bodyParser.urlencoded({extended:true})) app.post('/', (req, res)=> { res.json(Object.assign(req.body, {gotPost:true, ok:true})) }) app.post('/void', (req, res)=> { res.json('') }) app.put('/', (req, res)=> { res.json(Object.assign(req.body, {gotPut:true, ok:true})) }) app.patch('/', (req, res)=> { res.json(Object.assign(req.body, {gotPatch:true, ok:true})) }) app.delete('/', (req, res)=> { res.json(Object.assign(req.body, {gotDel:true, ok:true})) }) app.post('/goaway', (req, res) => { res.setHeader('location', '/') res.statusCode = 302 res.send() }) app.post('/boom', (req, res)=> { res.setHeader('test-header', 'foo') res.statusCode = 400 res.json({calls:3}) }) test('startup', t=> { t.plan(1) server = app.listen(3000, x=> { t.ok(true, 'started server') }) }) test('can post', t=> { t.plan(2) var url = 'http://localhost:3000/' var data = {a:1, b:new Date(Date.now()).toISOString()} tiny.post({url, data}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.ok(result.body.gotPost, 'got a post') console.log(result) } }) }) test('can post and handle "no content"', t=> { t.plan(2) var url = 'http://localhost:3000/void' var data = {a:1, b:new Date(Date.now()).toISOString()} tiny.post({url, data}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result (empty tho)') t.is(result.body, '') console.log(result) } }) }) test('can put', t=> { t.plan(2) var url = 'http://localhost:3000/' var data = {a:1, b:new Date(Date.now()).toISOString()} tiny.put({url, data}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.ok(result.body.gotPut, 'got a put') console.log(result) } }) }) test('can patch', t=> { t.plan(2) var url = 'http://localhost:3000/' var data = {a:1, b:new Date(Date.now()).toISOString()} tiny.patch({url, data}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.ok(result.body.gotPatch, 'got a patch') console.log(result) } }) }) test('can del', t=> { t.plan(3) var url = 'http://localhost:3000/' var data = {a:1, b:new Date(Date.now()).toISOString()} tiny.del({url, data}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.ok(result.body.gotDel, 'got a del') t.ok(result.body.a, 'passed params via query I guess') console.log(result) } }) }) test('can delete (aliased to del)', t=> { t.plan(3) var url = 'http://localhost:3000/' var data = {a:1, b:new Date(Date.now()).toISOString()} tiny.delete({url, data}, function __posted(err, result) { if (err) { t.fail(err) } else { t.ok(result, 'got a result') t.ok(result.body.gotDel, 'got a del') t.ok(result.body.a, 'passed params via query I guess') console.log(result) } }) }) test('can access response on errors', t=> { t.plan(5) var url = 'http://localhost:3000/boom' var data = {}; tiny.post({url, data}, function __posted(err, result) { t.ok(err, 'got an error') t.ok(err.raw, 'has raw response') t.equal(err.raw.statusCode, 400) t.equal(err.raw.headers['test-header'], 'foo') t.deepEqual(err.body, {calls: 3}) }) }) test('can handle redirects', t=> { t.plan(2) var url = 'http://localhost:3000/goaway' var data = {} tiny.post({url, data}, function __posted(err, result) { if (err) { t.fail(err) t.end() } else { t.ok(result, 'got a result') t.ok(result.headers.location, 'got a location header') console.log(result) } }) }) test('shutdown', t=> { t.plan(1) server.close() t.ok(true, 'closed server') }) ================================================ FILE: test-promise.js ================================================ var test = require('tape') var tiny = require('./dist.js') test('env', t=> { t.plan(7) t.ok(tiny, 'got a tiny') t.ok(tiny.get, 'got a tiny.get') t.ok(tiny.post, 'got a tiny.post') t.ok(tiny.put, 'got a tiny.put') t.ok(tiny.patch, 'got a tiny.patch') t.ok(tiny.del, 'got a tiny.delete') t.ok(tiny.delete, 'got a tiny.delete') console.log(tiny) }) test('can get a url', async t=> { t.plan(3) var url = 'https://brian.io' try { var result = await tiny.get({url}) t.ok(result, 'got a result') t.ok(result.headers, 'got headers') t.ok(result.body, 'got body') console.log(result) } catch(e) { t.fail(err.statusCode, 'failed to get') console.log(err) } }) test('get fails gracefully', t=> { t.plan(1) var url = 'http://nop333.ca' tiny.get({url}, function __ruhroh(err, result) { if (err) { t.ok(err, 'got err as expected') console.log(err) } else { t.fail(result, 'should not succeed') } }) }) test('bad url gives a acually useful error with a fucking line number holy shit', async t=> { t.plan(1) try { var res = await tiny.get('') t.fail(res) console.log(res) } catch(e) { t.ok(e, 'res') console.log(e) } }) ================================================ FILE: test-qq-multipart.js ================================================ let test = require('tape') let fs = require('fs') let path = require('path') let tiny = require('./dist.js') let http = require('http') let port = 3000 let host = 'localhost' let server test('make a multipart post', t=> { t.plan(1) t.ok(tiny, 'got env') }) test('start a fake server', t=> { t.plan(1) // somebody thought this was intuitive server = http.createServer((req, res)=> { let body = [] req.on('data', chunk => body.push(chunk)) req.on('end', function _end() { body = Buffer.concat(body).toString() res.end(body) }) }) server.listen({port, host}, err=> { if (err) t.fail(err) else t.pass(`Started server`) }) }) test('can multipart/form-data post', t=> { t.plan(1) let file = fs.readFileSync(path.join(__dirname, 'readme.md')) tiny.post({ url: `http://${host}:${port}`, headers: { 'content-type': 'multipart/form-data' }, data: { one: 1, file } }, function _post(err, data) { if (err) { t.fail(err, err) console.log(err) } else { t.ok(data.body.includes(file.toString()), 'posted') console.log(data) } }) }) test('close fake server', t=> { t.plan(1) server.close() t.ok(true, 'server closed') })