You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
9.0 KiB
253 lines
9.0 KiB
"use strict";
|
|
/**
|
|
* @license
|
|
* Copyright 2018 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.teenyRequest = exports.RequestError = void 0;
|
|
const node_fetch_1 = require("node-fetch");
|
|
const stream_1 = require("stream");
|
|
const uuid = require("uuid");
|
|
const agents_1 = require("./agents");
|
|
const TeenyStatistics_1 = require("./TeenyStatistics");
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const streamEvents = require('stream-events');
|
|
class RequestError extends Error {
|
|
}
|
|
exports.RequestError = RequestError;
|
|
/**
|
|
* Convert options from Request to Fetch format
|
|
* @private
|
|
* @param reqOpts Request options
|
|
*/
|
|
function requestToFetchOptions(reqOpts) {
|
|
const options = {
|
|
method: reqOpts.method || 'GET',
|
|
...(reqOpts.timeout && { timeout: reqOpts.timeout }),
|
|
...(typeof reqOpts.gzip === 'boolean' && { compress: reqOpts.gzip }),
|
|
};
|
|
if (typeof reqOpts.json === 'object') {
|
|
// Add Content-type: application/json header
|
|
reqOpts.headers = reqOpts.headers || {};
|
|
reqOpts.headers['Content-Type'] = 'application/json';
|
|
// Set body to JSON representation of value
|
|
options.body = JSON.stringify(reqOpts.json);
|
|
}
|
|
else {
|
|
if (Buffer.isBuffer(reqOpts.body)) {
|
|
options.body = reqOpts.body;
|
|
}
|
|
else if (typeof reqOpts.body !== 'string') {
|
|
options.body = JSON.stringify(reqOpts.body);
|
|
}
|
|
else {
|
|
options.body = reqOpts.body;
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
options.headers = reqOpts.headers;
|
|
let uri = (reqOpts.uri ||
|
|
reqOpts.url);
|
|
if (!uri) {
|
|
throw new Error('Missing uri or url in reqOpts.');
|
|
}
|
|
if (reqOpts.useQuerystring === true || typeof reqOpts.qs === 'object') {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const qs = require('querystring');
|
|
const params = qs.stringify(reqOpts.qs);
|
|
uri = uri + '?' + params;
|
|
}
|
|
options.agent = (0, agents_1.getAgent)(uri, reqOpts);
|
|
return { uri, options };
|
|
}
|
|
/**
|
|
* Convert a response from `fetch` to `request` format.
|
|
* @private
|
|
* @param opts The `request` options used to create the request.
|
|
* @param res The Fetch response
|
|
* @returns A `request` response object
|
|
*/
|
|
function fetchToRequestResponse(opts, res) {
|
|
const request = {};
|
|
request.agent = opts.agent || false;
|
|
request.headers = (opts.headers || {});
|
|
request.href = res.url;
|
|
// headers need to be converted from a map to an obj
|
|
const resHeaders = {};
|
|
res.headers.forEach((value, key) => (resHeaders[key] = value));
|
|
const response = Object.assign(res.body, {
|
|
statusCode: res.status,
|
|
statusMessage: res.statusText,
|
|
request,
|
|
body: res.body,
|
|
headers: resHeaders,
|
|
toJSON: () => ({ headers: resHeaders }),
|
|
});
|
|
return response;
|
|
}
|
|
/**
|
|
* Create POST body from two parts as multipart/related content-type
|
|
* @private
|
|
* @param boundary
|
|
* @param multipart
|
|
*/
|
|
function createMultipartStream(boundary, multipart) {
|
|
const finale = `--${boundary}--`;
|
|
const stream = new stream_1.PassThrough();
|
|
for (const part of multipart) {
|
|
const preamble = `--${boundary}\r\nContent-Type: ${part['Content-Type']}\r\n\r\n`;
|
|
stream.write(preamble);
|
|
if (typeof part.body === 'string') {
|
|
stream.write(part.body);
|
|
stream.write('\r\n');
|
|
}
|
|
else {
|
|
part.body.pipe(stream, { end: false });
|
|
part.body.on('end', () => {
|
|
stream.write('\r\n');
|
|
stream.write(finale);
|
|
stream.end();
|
|
});
|
|
}
|
|
}
|
|
return stream;
|
|
}
|
|
function teenyRequest(reqOpts, callback) {
|
|
const { uri, options } = requestToFetchOptions(reqOpts);
|
|
const multipart = reqOpts.multipart;
|
|
if (reqOpts.multipart && multipart.length === 2) {
|
|
if (!callback) {
|
|
// TODO: add support for multipart uploads through streaming
|
|
throw new Error('Multipart without callback is not implemented.');
|
|
}
|
|
const boundary = uuid.v4();
|
|
options.headers['Content-Type'] = `multipart/related; boundary=${boundary}`;
|
|
options.body = createMultipartStream(boundary, multipart);
|
|
// Multipart upload
|
|
teenyRequest.stats.requestStarting();
|
|
(0, node_fetch_1.default)(uri, options).then(res => {
|
|
teenyRequest.stats.requestFinished();
|
|
const header = res.headers.get('content-type');
|
|
const response = fetchToRequestResponse(options, res);
|
|
const body = response.body;
|
|
if (header === 'application/json' ||
|
|
header === 'application/json; charset=utf-8') {
|
|
res.json().then(json => {
|
|
response.body = json;
|
|
callback(null, response, json);
|
|
}, (err) => {
|
|
callback(err, response, body);
|
|
});
|
|
return;
|
|
}
|
|
res.text().then(text => {
|
|
response.body = text;
|
|
callback(null, response, text);
|
|
}, err => {
|
|
callback(err, response, body);
|
|
});
|
|
}, err => {
|
|
teenyRequest.stats.requestFinished();
|
|
callback(err, null, null);
|
|
});
|
|
return;
|
|
}
|
|
if (callback === undefined) {
|
|
// Stream mode
|
|
const requestStream = streamEvents(new stream_1.PassThrough());
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let responseStream;
|
|
requestStream.once('reading', () => {
|
|
if (responseStream) {
|
|
(0, stream_1.pipeline)(responseStream, requestStream, () => { });
|
|
}
|
|
else {
|
|
requestStream.once('response', () => {
|
|
(0, stream_1.pipeline)(responseStream, requestStream, () => { });
|
|
});
|
|
}
|
|
});
|
|
options.compress = false;
|
|
teenyRequest.stats.requestStarting();
|
|
(0, node_fetch_1.default)(uri, options).then(res => {
|
|
teenyRequest.stats.requestFinished();
|
|
responseStream = res.body;
|
|
responseStream.on('error', (err) => {
|
|
requestStream.emit('error', err);
|
|
});
|
|
const response = fetchToRequestResponse(options, res);
|
|
requestStream.emit('response', response);
|
|
}, err => {
|
|
teenyRequest.stats.requestFinished();
|
|
requestStream.emit('error', err);
|
|
});
|
|
// fetch doesn't supply the raw HTTP stream, instead it
|
|
// returns a PassThrough piped from the HTTP response
|
|
// stream.
|
|
return requestStream;
|
|
}
|
|
// GET or POST with callback
|
|
teenyRequest.stats.requestStarting();
|
|
(0, node_fetch_1.default)(uri, options).then(res => {
|
|
teenyRequest.stats.requestFinished();
|
|
const header = res.headers.get('content-type');
|
|
const response = fetchToRequestResponse(options, res);
|
|
const body = response.body;
|
|
if (header === 'application/json' ||
|
|
header === 'application/json; charset=utf-8') {
|
|
if (response.statusCode === 204) {
|
|
// Probably a DELETE
|
|
callback(null, response, body);
|
|
return;
|
|
}
|
|
res.json().then(json => {
|
|
response.body = json;
|
|
callback(null, response, json);
|
|
}, err => {
|
|
callback(err, response, body);
|
|
});
|
|
return;
|
|
}
|
|
res.text().then(text => {
|
|
const response = fetchToRequestResponse(options, res);
|
|
response.body = text;
|
|
callback(null, response, text);
|
|
}, err => {
|
|
callback(err, response, body);
|
|
});
|
|
}, err => {
|
|
teenyRequest.stats.requestFinished();
|
|
callback(err, null, null);
|
|
});
|
|
return;
|
|
}
|
|
exports.teenyRequest = teenyRequest;
|
|
teenyRequest.defaults = (defaults) => {
|
|
return (reqOpts, callback) => {
|
|
const opts = { ...defaults, ...reqOpts };
|
|
if (callback === undefined) {
|
|
return teenyRequest(opts);
|
|
}
|
|
teenyRequest(opts, callback);
|
|
};
|
|
};
|
|
/**
|
|
* Single instance of an interface for keeping track of things.
|
|
*/
|
|
teenyRequest.stats = new TeenyStatistics_1.TeenyStatistics();
|
|
teenyRequest.resetStats = () => {
|
|
teenyRequest.stats = new TeenyStatistics_1.TeenyStatistics(teenyRequest.stats.getOptions());
|
|
};
|
|
//# sourceMappingURL=index.js.map
|