Skip to content

Commit

Permalink
Merge pull request #204 from stoplightio/feature/use-isomorphic-fetch
Browse files Browse the repository at this point in the history
feat(http-resolver): use isomorphic-fetch for making requests
  • Loading branch information
Phil Sturgeon committed Jan 21, 2021
2 parents cc7ec57 + bb010c5 commit 551a51e
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 126 deletions.
2 changes: 1 addition & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ $RefParser.prototype.parse = async function parse (path, schema, options, callba
try {
let result = await promise;

if (result !== null && typeof result === "object" && !Buffer.isBuffer(result)) {
if (result !== null && typeof result === "object" && !ArrayBuffer.isView(result)) {
me.schema = result;
return maybe(args.callback, Promise.resolve(me.schema));
}
Expand Down
2 changes: 1 addition & 1 deletion lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,5 @@ function isEmpty (value) {
return value === undefined ||
(typeof value === "object" && Object.keys(value).length === 0) ||
(typeof value === "string" && value.trim().length === 0) ||
(Buffer.isBuffer(value) && value.length === 0);
(ArrayBuffer.isView(value) && value.length === 0);
}
10 changes: 5 additions & 5 deletions lib/parsers/binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module.exports = {
*/
canParse (file) {
// Use this parser if the file is a Buffer, and has a known binary extension
return Buffer.isBuffer(file.data) && BINARY_REGEXP.test(file.url);
return ArrayBuffer.isView(file.data) && BINARY_REGEXP.test(file.url);
},

/**
Expand All @@ -41,15 +41,15 @@ module.exports = {
* @param {string} file.url - The full URL of the referenced file
* @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
* @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
* @returns {Buffer}
* @returns {Uint8Array}
*/
parse (file) {
if (Buffer.isBuffer(file.data)) {
return file.data;
if (ArrayBuffer.isView(file.data)) {
return new Uint8Array(file.data);
}
else {
// This will reject if data is anything other than a string or typed array
return Buffer.from(file.data);
return new Uint8Array(Buffer.from(file.data));
}
}
};
7 changes: 5 additions & 2 deletions lib/parsers/json.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use strict";

const { ParserError } = require("../util/errors");
const TextDecoder = require("../util/text-decoder");

const decoder = new TextDecoder();

module.exports = {
/**
Expand Down Expand Up @@ -38,8 +41,8 @@ module.exports = {
*/
async parse (file) { // eslint-disable-line require-await
let data = file.data;
if (Buffer.isBuffer(data)) {
data = data.toString();
if (ArrayBuffer.isView(data)) {
data = decoder.decode(data);
}

if (typeof data === "string") {
Expand Down
8 changes: 5 additions & 3 deletions lib/parsers/text.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

const { ParserError } = require("../util/errors");
const TextDecoder = require("../util/text-decoder");

let TEXT_REGEXP = /\.(txt|htm|html|md|xml|js|min|map|css|scss|less|svg)$/i;

Expand Down Expand Up @@ -40,7 +41,7 @@ module.exports = {
*/
canParse (file) {
// Use this parser if the file is a string or Buffer, and has a known text-based extension
return (typeof file.data === "string" || Buffer.isBuffer(file.data)) && TEXT_REGEXP.test(file.url);
return (typeof file.data === "string" || ArrayBuffer.isView(file.data)) && TEXT_REGEXP.test(file.url);
},

/**
Expand All @@ -56,8 +57,9 @@ module.exports = {
if (typeof file.data === "string") {
return file.data;
}
else if (Buffer.isBuffer(file.data)) {
return file.data.toString(this.encoding);
else if (ArrayBuffer.isView(file.data)) {
let decoder = new TextDecoder(this.encoding);
return decoder.decode(file.data);
}
else {
throw new ParserError("data is not text", file.url);
Expand Down
7 changes: 5 additions & 2 deletions lib/parsers/yaml.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"use strict";

const { ParserError } = require("../util/errors");
const TextDecoder = require("../util/text-decoder");
const yaml = require("js-yaml");

const decoder = new TextDecoder();

module.exports = {
/**
* The order that this parser will run, in relation to other parsers.
Expand Down Expand Up @@ -39,8 +42,8 @@ module.exports = {
*/
async parse (file) { // eslint-disable-line require-await
let data = file.data;
if (Buffer.isBuffer(data)) {
data = data.toString();
if (ArrayBuffer.isView(data)) {
data = decoder.decode(data);
}

if (typeof data === "string") {
Expand Down
142 changes: 60 additions & 82 deletions lib/resolvers/http.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use strict";

const http = require("http");
const https = require("https");
const { ono } = require("@jsdevtools/ono");
const AbortController = require("abort-controller");
const fetch = require("isomorphic-fetch");
const url = require("../util/url");
const { ResolverError } = require("../util/errors");

Expand Down Expand Up @@ -70,7 +70,7 @@ module.exports = {
* @param {object} file - An object containing information about the referenced file
* @param {string} file.url - The full URL of the referenced file
* @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
* @returns {Promise<Buffer>}
* @returns {Promise<Uint8Array | string>}
*/
read (file) {
let u = url.parse(file.url);
Expand All @@ -80,7 +80,7 @@ module.exports = {
u.protocol = url.parse(location.href).protocol;
}

return download(u, this);
return download(u, this, []);
}
};

Expand All @@ -89,92 +89,70 @@ module.exports = {
*
* @param {Url|string} u - The url to download (can be a parsed {@link Url} object)
* @param {object} httpOptions - The `options.resolve.http` object
* @param {number} [redirects] - The redirect URLs that have already been followed
* @param {string[]} redirects - The redirect URLs that have already been followed
*
* @returns {Promise<Buffer>}
* @returns {Promise<Uint8Array | string>}
* The promise resolves with the raw downloaded data, or rejects if there is an HTTP error.
*/
function download (u, httpOptions, redirects) {
return new Promise(((resolve, reject) => {
u = url.parse(u);
redirects = redirects || [];
redirects.push(u.href);

get(u, httpOptions)
.then((res) => {
if (res.statusCode >= 400) {
throw ono({ status: res.statusCode }, `HTTP ERROR ${res.statusCode}`);
}
else if (res.statusCode >= 300) {
if (redirects.length > httpOptions.redirects) {
reject(new ResolverError(ono({ status: res.statusCode },
`Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`)));
}
else if (!res.headers.location) {
throw ono({ status: res.statusCode }, `HTTP ${res.statusCode} redirect with no location header`);
}
else {
// console.log('HTTP %d redirect %s -> %s', res.statusCode, u.href, res.headers.location);
let redirectTo = url.resolve(u, res.headers.location);
download(redirectTo, httpOptions, redirects).then(resolve, reject);
}
}
else {
resolve(res.body || Buffer.alloc(0));
}
})
.catch((err) => {
reject(new ResolverError(ono(err, `Error downloading ${u.href}`), u.href));
});
}));
}
async function download (u, httpOptions, redirects) {
u = url.parse(u);

redirects.push(u.href);

const controller = new AbortController();

/** @type {RequestInit} */
const init = {
method: "GET",
headers: httpOptions.headers || {},
credentials: httpOptions.withCredentials ? "include" : "omit",
signal: controller.signal,
// browser fetch API does not support redirects https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
redirect: process.browser ? "follow" : httpOptions.redirects === 0 ? "error" : "manual",
};

let timeout;
if (httpOptions.timeout > 0 && isFinite(httpOptions.timeout)) {
timeout = setTimeout(() => {
controller.abort();
}, httpOptions.timeout);
}

/**
* Sends an HTTP GET request.
*
* @param {Url} u - A parsed {@link Url} object
* @param {object} httpOptions - The `options.resolve.http` object
*
* @returns {Promise<Response>}
* The promise resolves with the HTTP Response object.
*/
function get (u, httpOptions) {
return new Promise(((resolve, reject) => {
// console.log('GET', u.href);

let protocol = u.protocol === "https:" ? https : http;
let req = protocol.get({
hostname: u.hostname,
port: u.port,
path: u.path,
auth: u.auth,
protocol: u.protocol,
headers: httpOptions.headers || {},
withCredentials: httpOptions.withCredentials
});

if (typeof req.setTimeout === "function") {
req.setTimeout(httpOptions.timeout);
}
try {
/** @type {Response} */
let res = await fetch(u.href, init);

req.on("timeout", () => {
req.abort();
});
if (res.status >= 300 && res.status < 400) {
if (redirects.length > httpOptions.redirects) {
throw new ResolverError(ono({ status: res.status },
`Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`));
}

req.on("error", reject);
let location = res.headers.get("Location");
if (!location) {
throw new ResolverError(ono({ status: res.status }, `HTTP ${res.status} redirect with no location header`));
}

req.once("response", (res) => {
res.body = Buffer.alloc(0);
let redirectTo = url.resolve(u, location);
return await download(redirectTo, httpOptions, redirects);
}

res.on("data", (data) => {
res.body = Buffer.concat([res.body, Buffer.from(data)]);
});
if (!res.ok) {
throw new Error(res.statusText);
}

res.on("error", reject);
return new Uint8Array(await res.arrayBuffer());
}
catch (err) {
if (err instanceof ResolverError) {
throw err;
}

res.on("end", () => {
resolve(res);
});
});
}));
throw new ResolverError(ono(err, `Error downloading ${u.href}`), u.href);
}
finally {
if (timeout !== undefined) {
clearTimeout(timeout);
}
}
}
5 changes: 5 additions & 0 deletions lib/util/text-decoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use strict";

const { TextDecoder: NodeTextDecoder } = require("fastestsmallesttextencoderdecoder");

module.exports = typeof TextDecoder === "undefined" ? NodeTextDecoder : TextDecoder;
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 551a51e

Please sign in to comment.