From 3461bee5fec1edba136dddb8f6aca6a07fdba7dc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 31 Aug 2022 17:19:02 -0600 Subject: [PATCH] feat: add fetch-based browser.js --- browser.js | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 2 files changed, 204 insertions(+) create mode 100644 browser.js diff --git a/browser.js b/browser.js new file mode 100644 index 0000000..d072e82 --- /dev/null +++ b/browser.js @@ -0,0 +1,201 @@ +'use strict'; + +// `fetch` will be available for node and browsers as a global +//var fetch = window.fetch; + +// https://developer.mozilla.org/en-US/docs/Web/API/fetch +// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch +let _fetchDefaults = { + method: 'GET', // *GET, POST, PATCH, PUT, DELETE, etc + headers: {}, + body: undefined, // String, ArrayBuffer, FormData, etc + mode: 'cors', // no-cors, *cors, same-origin + credentials: 'same-origin', // omit, *same-origin, include + cache: 'default', // *default, no-store, reload, no-cache, force-cache, only-if-cached + redirect: 'follow', // *follow, error, manual, + referrer: undefined, + referrerPolicy: 'no-referrer-when-downgrade', // no-referrer, *no-referrer-when-downgrade, same-origin, origin, strict-origin, origin-when-cross-origin, strict-origin-when-cross-origin, unsafe-url + integrity: '', + keepalive: false, + signal: null // +}; + +let _optionKeys = Object.keys(_fetchDefaults).concat([ + //'encoding', // N/A + //'stream', // TODO via getReader + //'json' // handled manually + //'form', // TODO + //'auth' // handled manually + //'formData', // TODO + //'FormData', // TODO + //'userAgent' // not allowed, non-standard for request.js +]); + +function setDefaults(_defs) { + return async function request(opts) { + if ('string' === typeof opts) { + opts = { url: opts }; + } + let reqOpts = { headers: {} }; + + if ( + opts.body || + (opts.json && true !== opts.json) || + opts.form || + opts.formData + ) { + // TODO this is probably a deviation from request's API + // need to check and probably eliminate it + reqOpts.method = (reqOpts.method || 'POST').toUpperCase(); + } else { + reqOpts.method = (reqOpts.method || 'GET').toUpperCase(); + } + + _optionKeys.forEach(function (key) { + if (key in opts) { + if ('undefined' !== typeof opts[key]) { + reqOpts[key] = opts[key]; + } + } else if (key in _defs) { + reqOpts[key] = _defs[key]; + } + }); + + if (opts.auth) { + // if opts.uri specifies auth it will be parsed by url.parse and passed directly to the http module + if ('string' !== typeof opts.auth) { + let u = opts.auth.user || opts.auth.username || ''; + let p = opts.auth.pass || opts.auth.password || ''; + reqOpts.headers.Authorization = encodeBasicAuth(`${u}:${p}`); + } else if ('string' === typeof opts.auth) { + reqOpts.headers.Authorization = encodeBasicAuth(`${opts.auth}`); + } + + // [request-compat] + if (opts.auth.bearer) { + // having a shortcut for base64 encoding makes sense, + // but this? Eh, whatevs... + reqOpts.header.Authorization = `Bearer ${opts.auth.bearer}`; + } + } + + let body; + if (opts.json && true !== opts.json) { + if (!opts.headers['content-type']) { + opts.headers['content-type'] = 'application/json'; + } + body = JSON.stringify(opts.json); + if (!opts.method) { + opts.method = 'POST'; + } + } + + // The node version will send HTTP Auth by default, but not Cookies. + // We don't have an equivalent option for `fetch`. Furthermore, + // `fetch` caches HTTP Auth Basic across browser refreshes, + // which is not analogous to the node behavior. + // + // "In the face of ambiguity, refuse the temptation to guess" + // + //if (!('credentials' in opts)) { + // opts.credentials = 'include'; + //} + + if (!('mode' in opts)) { + reqOpts.mode = 'cors'; + } + if (!('body' in opts)) { + if (body) { + reqOpts.body = body; + } + } + + let resp = await fetch(opts.url, reqOpts); + + let result = { + ok: resp.ok, + headers: headersToObj(resp.headers), + body: undefined, + // swapped to match request.js + statusCode: resp.status, + status: resp.statusText, + request: reqOpts, + response: resp + }; + result.toJSON = function () { + return { + ok: result.ok, + headers: result.headers, + body: result.body, + statusCode: result.statusCode, + status: result.status + }; + }; + + // return early if there's no body + if (!result.headers['content-type']) { + return result; + } + + // TODO blob, formData ? + if (null === opts.encoding) { + return await resp.arrayBuffer(); + } + + if (!opts.json) { + result.body = await resp.text(); + } else { + result.body = await resp.json().catch(async function () { + return await resp.text(); + }); + } + return result; + }; +} + +/** + * @param {Iterable.<*>} rheaders + * @returns {Object.} + */ +function headersToObj(rheaders) { + /* + Array.from(resp.headers.entries()).forEach(function (h) { + headers[h[0]] = h[1]; + }); + */ + let headerNames = Array.from(rheaders.keys()); + let resHeaders = {}; + headerNames.forEach(function (k) { + resHeaders[k] = rheaders.get(k); + }); + return resHeaders; +} + +/** + * @param {String} utf8 + * @returns {String} + */ +function encodeBasicAuth(utf8) { + let b64 = unicodeToBase64(utf8); + return `Basic ${b64}`; +} + +/** + * @param {String} utf8 + * @returns {String} + */ +function unicodeToBase64(utf8) { + let str = ''; + let uint8 = new TextEncoder().encode(utf8); + uint8.forEach(function (b) { + str += String.fromCharCode(b); + }); + let b64 = btoa(str); + return b64; +} + +let defaultRequest = setDefaults({ mode: 'cors' }); +exports.request = defaultRequest; +exports.defaults = setDefaults; +module.exports = defaultRequest; +module.exports.defaults = setDefaults; diff --git a/package.json b/package.json index 62be29d..1c963a7 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.8.3", "description": "A lightweight, zero-dependency drop-in replacement for request", "main": "index.js", + "browser": { + "index.js": "browser.js" + }, "files": [ "lib" ],