UNPKG

@sap-cloud-sdk/http-client

Version:

SAP Cloud SDK for JavaScript http-client

380 lines • 17.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.encodeAllParameters = exports.oDataTypedClientParameterEncoder = void 0; exports.buildHttpRequest = buildHttpRequest; exports.execute = execute; exports.buildHttpRequestConfigWithOrigin = buildHttpRequestConfigWithOrigin; exports.buildRequestWithMergedHeadersAndQueryParameters = buildRequestWithMergedHeadersAndQueryParameters; exports.executeHttpRequest = executeHttpRequest; exports.executeHttpRequestWithOrigin = executeHttpRequestWithOrigin; exports.getAxiosConfigWithDefaults = getAxiosConfigWithDefaults; exports.getAxiosConfigWithDefaultsWithoutMethod = getAxiosConfigWithDefaultsWithoutMethod; exports.getDefaultHttpRequestConfigOptions = getDefaultHttpRequestConfigOptions; exports.getDefaultHttpRequestOptions = getDefaultHttpRequestOptions; const http = __importStar(require("http")); const https = __importStar(require("https")); const connectivity_1 = require("@sap-cloud-sdk/connectivity"); const internal_1 = require("@sap-cloud-sdk/connectivity/internal"); const internal_2 = require("@sap-cloud-sdk/resilience/internal"); const util_1 = require("@sap-cloud-sdk/util"); const axios_1 = __importDefault(require("axios")); const http_client_types_1 = require("./http-client-types"); const http_request_config_1 = require("./http-request-config"); const csrf_token_middleware_1 = require("./csrf-token-middleware"); const logger = (0, util_1.createLogger)({ package: 'http-client', messageContext: 'http-client' }); /** * Builds a {@link DestinationHttpRequestConfig} for the given destination. * If a destination name (and a JWT) are provided, it will try to resolve the destination. * @param destination - A destination or a destination name and a JWT. * @returns A {@link DestinationHttpRequestConfig}. */ async function buildHttpRequest(destination) { const resolvedDestination = await (0, internal_1.resolveDestination)(destination); (0, internal_1.assertHttpDestination)(resolvedDestination); const headers = await buildHeaders(resolvedDestination); return buildDestinationHttpRequestConfig(resolvedDestination, headers); } /** * Takes as parameter a function that expects an {@link HttpRequest} and returns a Promise of {@link HttpResponse}. * Returns a function that takes a destination and a request-config (extends {@link HttpRequestConfig}), builds an {@link HttpRequest} from them, and calls * the provided execute function. * * NOTE: If you simply want to execute a request without passing your own execute function, use {@link executeHttpRequest} instead. * @param executeFn - A function that can execute an {@link HttpRequestConfig}. * @returns A function expecting destination and a request. * @internal */ function execute(executeFn) { return async function (destination, requestConfig, options) { const resolvedDestination = await (0, internal_1.resolveDestination)(destination); (0, internal_1.assertHttpDestination)(resolvedDestination); const destinationRequestConfig = await buildHttpRequest(resolvedDestination); logCustomHeadersWarning(requestConfig.headers); const request = await buildRequestWithMergedHeadersAndQueryParameters(requestConfig, resolvedDestination, destinationRequestConfig, destination.jwt); if (options?.fetchCsrfToken) { requestConfig.middleware = [...(requestConfig.middleware || []), (0, csrf_token_middleware_1.csrf)()]; } return (0, internal_2.executeWithMiddleware)(requestConfig.middleware, { fnArgument: request, fn: (req) => { logRequestInformation(request); return executeFn(req); }, context: { jwt: destination.jwt, uri: resolvedDestination.url, destinationName: resolvedDestination.name ?? undefined, tenantId: (0, connectivity_1.getTenantId)(destination.jwt) } }); }; } /** * Build an {@link HttpRequestConfigWithOrigin} from a given {@link HttpRequestConfigWithOrigin} or {@link HttpRequestConfig} * @param requestConfig - The given {@link HttpRequestConfigWithOrigin} or {@link HttpRequestConfig} * @returns The resulting {@link HttpRequestConfigWithOrigin} * @internal */ function buildHttpRequestConfigWithOrigin(requestConfig) { if (!requestConfig) { return getDefaultHttpRequestConfigOptions(); } if ((0, http_client_types_1.isHttpRequestConfigWithOrigin)(requestConfig)) { return requestConfig; } return { ...requestConfig, headers: { requestConfig: {}, ...(requestConfig.headers && { custom: requestConfig.headers }) }, params: { requestConfig: {}, ...(requestConfig.params && { custom: requestConfig.params }) } }; } /** * This method does nothing and is only there to indicate that the call was made by a typed OData client and encoding already happened in the client. * @param params - Parameters which are returned. * @returns The parameters as they are without encoding. * @internal */ const oDataTypedClientParameterEncoder = (params) => params; exports.oDataTypedClientParameterEncoder = oDataTypedClientParameterEncoder; function encodeQueryParameters(options) { const { parameterEncoder, parameters, exclude } = options; return Object.fromEntries(Object.entries(parameters).map(([key, value]) => exclude.includes(key) ? [key, value] : [key, value ? parameterEncoder(value) : value])); } function isOdataTypedClientParameterEncoder(parameterEncoder) { return parameterEncoder.name === exports.oDataTypedClientParameterEncoder.name; } function getEncodedParameters(parameters, requestConfig) { const { parameterEncoder } = requestConfig; if ((0, util_1.isNullish)(parameterEncoder)) { return encodeQueryParameters({ parameters, parameterEncoder: exports.encodeAllParameters, exclude: ['custom'] }); } if (isOdataTypedClientParameterEncoder(parameterEncoder)) { return encodeQueryParameters({ parameters, parameterEncoder: exports.encodeAllParameters, exclude: ['custom', 'requestConfig'] }); } // Custom encoder provided by user -> use it for all origins return encodeQueryParameters({ parameters, parameterEncoder, exclude: [] }); } /** * @internal * Build a request config from a given request config and a destination. * In addition to merging the information from the request config and the destination, it also picks values with higher priority for headers and query parameters. * @param requestConfig - Any object representing an HTTP request. * @param destination - A resolved {@link Destination} object. * @param destinationRequestConfig - A {@link DestinationHttpRequestConfig} object, that is built from a {@link Destination}. * @see {@link mergeOptionsWithPriority} * @returns A resulting request config. */ async function buildRequestWithMergedHeadersAndQueryParameters(requestConfig, destination, destinationRequestConfig, jwt) { const { paramsOriginOptions, headersOriginOptions, requestConfigBase } = splitRequestConfig(requestConfig); const parameters = collectParametersFromAllOrigins(destination, paramsOriginOptions); const encodedParameters = getEncodedParameters(parameters, requestConfig); const mergedQueryParameter = (0, http_request_config_1.mergeOptionsWithPriority)(encodedParameters); const mergedHeaders = await getMergedHeaders(destination, destinationRequestConfig.headers, headersOriginOptions, jwt); const request = merge(destinationRequestConfig, requestConfigBase); request.headers = mergedHeaders || {}; request.params = mergedQueryParameter || {}; return request; } async function getMergedHeaders(destination, headersDestination, headersOriginOptions, jwt) { const queryParametersDestinationProperty = (0, internal_1.getAdditionalHeaders)(destination.originalProperties || {}).headers; headersDestination = destination.forwardAuthToken ? addForwardAuthTokenHeader(headersDestination, jwt) : headersDestination; return (0, http_request_config_1.mergeOptionsWithPriority)({ requestConfig: headersOriginOptions?.requestConfig, custom: { ...headersOriginOptions?.custom }, destinationProperty: queryParametersDestinationProperty, destination: headersDestination }); } function addForwardAuthTokenHeader(headersDestination, jwt) { if (jwt) { return { ...headersDestination, authorization: `Bearer ${jwt}` }; } logger.debug('The `forwardAuthToken` is set, but the JWT is missing. Please provide a valid JWT to enable token forwarding.'); return headersDestination; } function collectParametersFromAllOrigins(destination, paramsOriginOptions) { const queryParametersDestinationProperty = (0, internal_1.getAdditionalQueryParameters)(destination.originalProperties || {}).queryParameters; return { ...paramsOriginOptions, destinationProperty: queryParametersDestinationProperty, destination: destination.queryParameters }; } function splitRequestConfig(requestConfig) { const paramsOriginOptions = requestConfig.params; const headersOriginOptions = requestConfig.headers; return { paramsOriginOptions, headersOriginOptions, requestConfigBase: requestConfig }; } function logCustomHeadersWarning(headers) { if (!headers) { return; } const customHeaders = headers.custom; const requestConfigHeaders = headers.requestConfig; if (customHeaders && requestConfigHeaders) { const headerKeysToBeOverwritten = Object.keys(customHeaders).filter(customHeaderKey => Object.keys(requestConfigHeaders).includes(customHeaderKey)); if (headerKeysToBeOverwritten.length) { logger.debug(`The following custom headers will overwrite headers created by the SDK, if they use the same key:\n${headerKeysToBeOverwritten .map(key => ` - "${key}"`) .join('\n')} If the parameters from multiple origins use the same key, the priority is 1. Custom, 2. Destination, 3. Internal.`); } } } function logRequestInformation(request) { const basicRequestInfo = `Execute '${request.method}' request with target: ${request.url}.`; if (request.headers) { const headerText = Object.entries((0, util_1.sanitizeRecord)(request.headers)) .map(([key, value]) => `${key}:${value}`) .join(util_1.unixEOL); logger.debug(`${basicRequestInfo}${util_1.unixEOL}The headers of the request are:${util_1.unixEOL}${headerText}`); } else { logger.debug(basicRequestInfo); } } // eslint-disable-next-line jsdoc/require-returns-check /** * Builds a {@link DestinationHttpRequestConfig} for the given destination, merges it into the given `requestConfig` and executes it (using Axios). * @param destination - A destination or a destination name and a JWT. * @param requestConfig - Any object representing an HTTP request. * @param options - An {@link HttpRequestOptions} of the HTTP request for configuring e.g., CSRF token delegation. By default, the SDK will fetch the CSRF token. * @returns A promise resolving to an {@link HttpResponse}. */ function executeHttpRequest(destination, requestConfig, options) { const requestConfigWithOrigin = buildHttpRequestConfigWithOrigin(requestConfig); return execute(executeWithAxios)(destination, requestConfigWithOrigin, { ...getDefaultHttpRequestOptions(), ...options }); } /** * Builds a {@link DestinationHttpRequestConfig} for the given destination, merges it into the given {@link HttpRequestConfigWithOrigin} * and executes it (using Axios). * The {@link HttpRequestConfigWithOrigin} supports defining header options and query parameter options with origins. * Equally named headers and query parameters are prioritized in the following order: * 1. `custom` * 2. Destination related headers/query parameters * 3. `requestConfig`. * @param destination - A destination or a destination name and a JWT. * @param requestConfig - Any object representing an HTTP request. * @param options - An {@link HttpRequestOptions} of the HTTP request for configuring e.g., CSRF token delegation. By default, the SDK will fetch the CSRF token. * @returns A promise resolving to an {@link HttpResponse}. * @see https://sap.github.io/cloud-sdk/docs/js/features/connectivity/query-parameters */ function executeHttpRequestWithOrigin(destination, requestConfig, options) { const requestConfigWithDefaults = requestConfig ?? getDefaultHttpRequestConfigOptions(); return execute(executeWithAxios)(destination, requestConfigWithDefaults, { ...getDefaultHttpRequestOptions(), ...options }); } async function buildDestinationHttpRequestConfig(destination, headers) { return { baseURL: destination.url, headers, params: destination.queryParameters, proxy: (0, internal_1.getProxyConfig)(destination), ...(await (0, connectivity_1.getAgentConfig)(destination)) }; } async function buildHeaders(destination) { try { return await (0, connectivity_1.buildHeadersForDestination)(destination); } catch (error) { throw new util_1.ErrorWithCause('Failed to build headers.', error); } } function merge(destinationRequestConfig, customRequestConfig) { return { ...destinationRequestConfig, ...customRequestConfig, headers: { ...destinationRequestConfig.headers, ...customRequestConfig.headers } }; } function mergeRequestWithAxiosDefaults(request) { return { ...getAxiosConfigWithDefaults(), ...request }; } function executeWithAxios(request) { return axios_1.default.request(mergeRequestWithAxiosDefaults(request)); } /** * Builds an Axios config with default configuration i.e. no_proxy, default http and https agent and GET as request method. * @returns RawAxiosRequestConfig with default parameters * @internal */ function getAxiosConfigWithDefaults() { return { ...getAxiosConfigWithDefaultsWithoutMethod(), method: 'get' }; } /** * @internal */ function getAxiosConfigWithDefaultsWithoutMethod() { return { httpAgent: new http.Agent(), httpsAgent: new https.Agent(), timeout: 0, // zero means no timeout https://github.com/axios/axios/blob/main/README.md#request-config paramsSerializer: { serialize: (params = {}) => Object.entries(params) .map(([key, value]) => `${key}=${value}`) .join('&') } }; } /** * @internal */ function getDefaultHttpRequestConfigOptions() { return { method: 'get' }; } /** * @internal */ function getDefaultHttpRequestOptions() { return { fetchCsrfToken: true }; } /** * Encoder for encoding all query parameters (key and value) using encodeURIComponent. * @param parameter - Parameter to be encoded using encodeURIComponent. * @returns Encoded parameter object. */ const encodeAllParameters = function (parameter) { return Object.fromEntries(Object.entries(parameter).map(([key, value]) => [ encodeURIComponent(key), encodeURIComponent(value) ])); }; exports.encodeAllParameters = encodeAllParameters; //# sourceMappingURL=http-client.js.map