UNPKG

@usebruno/cli

Version:

With Bruno CLI, you can now run your API collections with ease using simple command line commands.

212 lines (181 loc) 7.47 kB
const axios = require('axios'); const { CLI_VERSION } = require('../constants'); const { addCookieToJar, getCookieStringForUrl } = require('./cookies'); const { createFormData } = require('./form-data'); const { setupProxyAgents } = require('./proxy-util'); const redirectResponseCodes = [301, 302, 303, 307, 308]; const METHOD_CHANGING_REDIRECTS = [301, 302, 303]; const saveCookies = (url, headers) => { if (headers['set-cookie']) { let setCookieHeaders = Array.isArray(headers['set-cookie']) ? headers['set-cookie'] : [headers['set-cookie']]; for (let setCookieHeader of setCookieHeaders) { if (typeof setCookieHeader === 'string' && setCookieHeader.length) { addCookieToJar(setCookieHeader, url); } } } }; const createRedirectConfig = (error, redirectUrl) => { const requestConfig = { ...error.config, url: redirectUrl, headers: { ...error.config.headers } }; const statusCode = error.response.status; const originalMethod = (error.config.method || 'get').toLowerCase(); // For 301, 302, 303: change method to GET unless it was HEAD if (METHOD_CHANGING_REDIRECTS.includes(statusCode) && originalMethod !== 'head') { requestConfig.method = 'get'; requestConfig.data = undefined; // Clean up headers that are no longer relevant delete requestConfig.headers['content-length']; delete requestConfig.headers['Content-Length']; delete requestConfig.headers['content-type']; delete requestConfig.headers['Content-Type']; } else { // For 307, 308 and other status codes: preserve method and body if (requestConfig.data && typeof requestConfig.data === 'object' && requestConfig.data.constructor && requestConfig.data.constructor.name === 'FormData') { const formData = requestConfig.data; if (formData._released || (formData._streams && formData._streams.length === 0)) { if (error.config._originalMultipartData && error.config.collectionPath) { const recreatedForm = createFormData(error.config._originalMultipartData, error.config.collectionPath); requestConfig.data = recreatedForm; const formHeaders = recreatedForm.getHeaders(); Object.assign(requestConfig.headers, formHeaders); // preserve the original data for potential future redirects requestConfig._originalMultipartData = error.config._originalMultipartData; requestConfig.collectionPath = error.config.collectionPath; } } else { requestConfig._originalMultipartData = error.config._originalMultipartData; requestConfig.collectionPath = error.config.collectionPath; } } } return requestConfig; }; /** * Function that configures axios with timing interceptors * Important to note here that the timings are not completely accurate. * @see https://github.com/axios/axios/issues/695 * @returns {axios.AxiosInstance} */ function makeAxiosInstance({ requestMaxRedirects = 5, disableCookies, followRedirects = true, proxyMode, proxyConfig, systemProxyConfig, httpsAgentRequestFields, interpolationOptions, disableCache } = {}) { let redirectCount = 0; /** @type {axios.AxiosInstance} */ const instance = axios.create({ proxy: false, maxRedirects: 0, headers: {} }); // Extend common headers with User-Agent rather than replacing the object. // axios.create() preserves defaults.headers.common = { Accept: 'application/json, text/plain, */*' }. // Assigning a new object (= { 'User-Agent': ... }) would nuke that default, causing servers that // rely on content-negotiation to receive requests with no Accept header. instance.defaults.headers.common['User-Agent'] = `bruno-runtime/${CLI_VERSION}`; instance.interceptors.request.use((config) => { config.headers['request-start-time'] = Date.now(); /** Apply header deletions requested via req.deleteHeader() in pre-request scripts. Using set(name, null) rather than delete(): the axios http adapter guards its own defaults (User-Agent, Accept-Encoding) with set(..., false) which only skips writing when the key already exists. delete() removes the key entirely, so the guard misses and the adapter re-adds the default. null keeps the key present (blocking the guard) while toJSON() omits null values from the wire. */ const headersToDelete = config.__headersToDelete; if (headersToDelete && Array.isArray(headersToDelete)) { headersToDelete.forEach((headerName) => { const lower = headerName.toLowerCase(); if (lower === 'host' || lower === 'connection') return; config.headers.set(headerName, null); }); delete config.__headersToDelete; } // Add cookies to request if available and not disabled if (!disableCookies) { const cookieString = getCookieStringForUrl(config.url); if (cookieString && typeof cookieString === 'string' && cookieString.length) { config.headers['cookie'] = cookieString; } } return config; }); instance.interceptors.response.use( (response) => { const end = Date.now(); const start = response.config.headers['request-start-time']; response.headers['request-duration'] = end - start; redirectCount = 0; return response; }, (error) => { if (error.response) { const end = Date.now(); const start = error.config.headers['request-start-time']; error.response.headers['request-duration'] = end - start; if (redirectResponseCodes.includes(error.response.status)) { if (!followRedirects) { if (!disableCookies) { saveCookies(error.config.url, error.response.headers); } return Promise.reject(error); } if (redirectCount >= requestMaxRedirects) { // todo: needs to be discussed whether the original error response message should be modified or not return Promise.reject(error); } const locationHeader = error.response.headers.location; if (!locationHeader) { // todo: needs to be discussed whether the original error response message should be modified or not return Promise.reject(error); } redirectCount++; let redirectUrl = locationHeader; if (!locationHeader.match(/^https?:\/\//i)) { const URL = require('url'); redirectUrl = URL.resolve(error.config.url, locationHeader); } if (!disableCookies) { saveCookies(error.config.url, error.response.headers); } const requestConfig = createRedirectConfig(error, redirectUrl); setupProxyAgents({ requestConfig, proxyMode, proxyConfig, systemProxyConfig, httpsAgentRequestFields, interpolationOptions, disableCache }); if (!disableCookies) { const cookieString = getCookieStringForUrl(redirectUrl); if (cookieString && typeof cookieString === 'string' && cookieString.length) { requestConfig.headers['cookie'] = cookieString; } } return instance(requestConfig); } } return Promise.reject(error); } ); return instance; } module.exports = { makeAxiosInstance };