@sap-cloud-sdk/http-client
Version:
SAP Cloud SDK for JavaScript http-client
380 lines • 17.1 kB
JavaScript
;
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