UNPKG

node-libcurl

Version:

The fastest http(s) client (and much more) for Node.js - Node.js bindings for libcurl

318 lines (317 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.curly = void 0; /** * Copyright (c) Jonathan Cardoso Machado. All Rights Reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ require("./moduleSetup"); const CurlOption_1 = require("./generated/CurlOption"); const Curl_1 = require("./Curl"); const CurlFeature_1 = require("./enum/CurlFeature"); const CurlError_1 = require("./CurlError"); const CurlEasyError_1 = require("./CurlEasyError"); // This is basically http.METHODS const methods = [ 'acl', 'bind', 'checkout', 'connect', 'copy', 'delete', 'get', 'head', 'link', 'lock', 'm-search', 'merge', 'mkactivity', 'mkcalendar', 'mkcol', 'move', 'notify', 'options', 'patch', 'post', 'propfind', 'proppatch', 'purge', 'put', 'rebind', 'report', 'search', 'source', 'subscribe', 'trace', 'unbind', 'unlink', 'unlock', 'unsubscribe', ]; const create = (defaultOptions = {}) => { // Object pool for Curl instances let poolLimit = 0; const pool = []; const getFromPool = () => { if (poolLimit === 0) { // Pooling disabled, create new instance return new Curl_1.Curl(); } if (pool.length > 0) { // Get from pool return pool.pop(); } // Pool empty, create new instance return new Curl_1.Curl(); }; const returnToPool = (curlHandle) => { if (poolLimit === 0) { // Pooling disabled, close the handle curlHandle.close(); return; } if (pool.length < poolLimit) { // Reset the handle for reuse curlHandle.reset(); pool.push(curlHandle); } else { // Pool full, close the handle curlHandle.close(); } }; const setObjectPoolLimit = (limit) => { poolLimit = limit; if (limit <= 0) { for (const handle of pool) { handle.close(); } pool.length = 0; } else { // If reducing limit, close excess instances while (pool.length > limit) { const handle = pool.pop(); handle.close(); } } }; function curly(url, options = {}) { const curlHandle = getFromPool(); curlHandle.enable(CurlFeature_1.CurlFeature.NoDataParsing); curlHandle.setOpt('URL', `${options.curlyBaseUrl || ''}${url}`); const finalOptions = { ...defaultOptions, ...options, }; for (const key of Object.keys(finalOptions)) { const keyTyped = key; const optionName = keyTyped in CurlOption_1.CurlOptionCamelCaseMap ? CurlOption_1.CurlOptionCamelCaseMap[keyTyped] : keyTyped; // if it begins with curly we do not set it on the curlHandle // as it's an specific option for curly if (optionName.startsWith('curly')) continue; // @ts-ignore @TODO Try to type this curlHandle.setOpt(optionName, finalOptions[key]); } // streams! const { curlyStreamResponse, curlyStreamResponseHighWaterMark, curlyStreamUpload, curlyMimePost, } = finalOptions; const isUsingStream = !!(curlyStreamResponse || curlyStreamUpload); if (finalOptions.curlyProgressCallback) { if (typeof finalOptions.curlyProgressCallback !== 'function') { throw new TypeError('curlyProgressCallback must be a function with signature (number, number, number, number) => number'); } const fnToCall = isUsingStream ? 'setStreamProgressCallback' : 'setProgressCallback'; curlHandle[fnToCall](finalOptions.curlyProgressCallback); } if (curlyStreamResponse) { curlHandle.enable(CurlFeature_1.CurlFeature.StreamResponse); if (curlyStreamResponseHighWaterMark) { curlHandle.setStreamResponseHighWaterMark(curlyStreamResponseHighWaterMark); } } if (curlyStreamUpload) { curlHandle.setUploadStream(curlyStreamUpload); } if (curlyMimePost && curlyMimePost.length > 0) { curlHandle.setMimePost(curlyMimePost); } const lowerCaseHeadersIfNecessary = (headers) => { // in-place modification // yeah, I know mutability is bad and all that if (finalOptions.curlyLowerCaseHeaders) { for (const headersReq of headers) { const entries = Object.entries(headersReq); for (const [headerKey, headerValue] of entries) { delete headersReq[headerKey]; // @ts-expect-error ignoring this for now headersReq[headerKey.toLowerCase()] = headerValue; } } } }; return new Promise((resolve, reject) => { let stream; if (curlyStreamResponse) { curlHandle.on('stream', (_stream, statusCode, headers) => { lowerCaseHeadersIfNecessary(headers); stream = _stream; resolve({ // @ts-ignore cannot be subtype yada yada data: stream, statusCode, headers, }); }); } curlHandle.on('end', (statusCode, data, headers) => { returnToPool(curlHandle); // only need to the remaining here if we did not enabled // the stream response if (curlyStreamResponse) { return; } const contentTypeEntry = headers.length > 0 ? Object.entries(headers[headers.length - 1]).find(([k]) => k.toLowerCase() === 'content-type') : null; let contentType = (contentTypeEntry ? contentTypeEntry[1] : ''); // remove the metadata of the content-type, like charset // See https://tools.ietf.org/html/rfc7231#section-3.1.1.5 contentType = contentType.split(';')[0]; const responseBodyParsers = { ...curly.defaultResponseBodyParsers, ...finalOptions.curlyResponseBodyParsers, }; let foundParser = finalOptions.curlyResponseBodyParser; if (typeof foundParser === 'undefined') { for (const [contentTypeFormat, parser] of Object.entries(responseBodyParsers)) { if (typeof parser !== 'function') { return reject(new TypeError(`Response body parser for ${contentTypeFormat} must be a function`)); } if (contentType === contentTypeFormat) { foundParser = parser; break; } else if (contentTypeFormat === '*') { foundParser = parser; break; } else { const partsFormat = contentTypeFormat.split('/'); const partsContentType = contentType.split('/'); if (partsContentType.length === partsFormat.length && partsContentType.every((val, index) => partsFormat[index] === '*' || partsFormat[index] === val)) { foundParser = parser; break; } } } } if (foundParser && typeof foundParser !== 'function') { return reject(new TypeError('`curlyResponseBodyParser` passed to curly must be false or a function.')); } lowerCaseHeadersIfNecessary(headers); try { resolve({ statusCode: statusCode, data: foundParser ? foundParser(data, headers) : data, headers: headers, }); } catch (error) { reject(error); } }); curlHandle.on('error', (error, errorCode) => { returnToPool(curlHandle); const errorToUse = error instanceof CurlError_1.CurlError ? error : new CurlEasyError_1.CurlEasyError(error.message, errorCode, { cause: error }); // oops, if have a stream it means the promise // has been resolved with it // so instead of rejecting the original promise // we are emitting the error event on the stream if (stream) { stream.emit('error', errorToUse); } else { reject(errorToUse); } }); try { curlHandle.perform(); } catch (error) /* istanbul ignore next: this should never happen 🤷‍♂️ */ { returnToPool(curlHandle); reject(error); } }); } curly.create = create; curly.setObjectPoolLimit = setObjectPoolLimit; curly.defaultResponseBodyParsers = { 'application/json': (data, _headers) => { try { const string = data.toString('utf8'); return JSON.parse(string); } catch { throw new Error(`curly failed to parse "application/json" content as JSON. This is generally caused by receiving malformed JSON data from the server. You can disable this automatic behavior by setting the option curlyResponseBodyParser to false, then a Buffer will be returned as the data. You can also overwrite the "application/json" parser with your own by changing one of the following: - curly.defaultResponseBodyParsers['application/json'] or - options.curlyResponseBodyParsers = { 'application/json': parser } If you want just a single function to handle all content-types, you can use the option "curlyResponseBodyParser". `); } }, // We are in [INSERT CURRENT YEAR], let's assume everyone is using utf8 encoding for text/* content-type. 'text/*': (data, _headers) => data.toString('utf8'), // otherwise let's just return the raw buffer '*': (data, _headers) => data, }; const httpMethodOptionsMap = { get: null, post: (_m, o) => ({ post: true, ...o, }), head: (_m, o) => ({ nobody: true, ...o, }), _: (m, o) => ({ customRequest: m.toUpperCase(), ...o, }), }; for (const httpMethod of methods) { const httpMethodOptionsKey = Object.prototype.hasOwnProperty.call(httpMethodOptionsMap, httpMethod) ? httpMethod : '_'; const httpMethodOptions = httpMethodOptionsMap[httpMethodOptionsKey]; // @ts-ignore curly[httpMethod] = httpMethodOptions === null ? curly : (url, options = {}) => curly(url, { ...httpMethodOptions(httpMethod, options), }); } // @ts-ignore return curly; }; /** * Curly function * * @public */ exports.curly = create(); //# sourceMappingURL=curly.js.map