@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
159 lines (144 loc) • 5.23 kB
JavaScript
const { inspect } = require('util');
const debug = require('../../cds').log('req');
function stacklessError(message) {
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error(message);
Error.stackTraceLimit = stackTraceLimit;
return error;
}
function extractDetails(data) {
if (!data) return undefined;
if (typeof data === 'string') {
if (/<html/i.test(data)) {
return '(HTML response received. If applicable, ensure the route to MTX is configured correctly in App Router.)';
}
if (data.length >= 1000) return undefined;
return data.trim();
}
if (typeof data === 'object') {
if (data.error_description) return data.error_description;
if (data.error && typeof data.error === 'object' && data.error.message) return data.error.message;
if (data.error && typeof data.error === 'string') return data.error;
if (typeof data.message === 'string') return data.message;
return inspect(data);
}
return undefined;
}
function buildError(method, url, { status, statusText, data, errno, code, fetchError } = {}) {
const obfuscateUrl = url => url?.replace(/(passcode|refreshToken|clientsecret|key)=[^&]+/g, '$1=...');
const prefix = (url && method ? `${method.toUpperCase()} ${obfuscateUrl(url)}` : 'Request') + ' failed';
let error;
if (errno || code) {
const errInfo = [errno, code].filter(Boolean).join(' ');
const message = prefix +
`: ${errInfo}` +
(/\b(ENOTFOUND|EAI_AGAIN)\b/.test(code)
? `. Make sure the URL is correct and the server is running.`
: '');
error = stacklessError(message);
} else {
const details = extractDetails(data);
const reason = typeof data?.error === 'string' ? data.error : undefined;
const errObj = debug._debug && typeof data?.error === 'object'
? inspect(data.error)
: undefined;
const message = prefix +
(status || statusText || reason || details
? (status || statusText ? ':' : '') +
(status ? ` ${status}` : '') +
(statusText ? ` ${statusText}` : '') +
(reason ? `. ${reason}` : '') +
(details ? `. Details: ${details}` : '') +
(errObj ? `. Error object: ${errObj}` : '')
: '');
error = stacklessError(message);
if (status) error.status = status;
if (data?.passcode_url) {
error.auth = { passcode_url: data.passcode_url };
}
}
if (debug._debug && fetchError) {
error.cause = fetchError;
}
return error;
}
async function parseResponseBody(response) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
const text = await response.text();
try {
return JSON.parse(text);
} catch { /* ignore */ }
return text;
}
/**
* Build a fetch error from a non-ok response.
* Reads the response body and constructs a rich error with .status, .message, and .auth.
*
* @param {string} method - HTTP method
* @param {string} url - Request URL
* @param {Response} response - Fetch Response object
* @returns {Promise<Error>} error with .status, .message, .auth properties
*/
async function buildResponseError(method, url, response) {
let data;
try {
data = await parseResponseBody(response);
} catch { /* ignore */ }
return buildError(method, url, {
status: response.status,
statusText: response.statusText,
data,
fetchError: new Error(`${response.status} ${response.statusText}`)
});
}
/**
* Build a fetch error from a network/system error.
*
* @param {string} method - HTTP method
* @param {string} url - Request URL
* @param {Error} fetchError - The caught error from fetch()
* @returns {Error} error with formatted message
*/
function buildNetworkError(method, url, fetchError) {
const cause = fetchError.cause ?? {};
const errno = fetchError.errno || cause.errno;
const code = fetchError.code || cause.code
|| fetchError.message?.match(/\b(ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH)\b/)?.[1]
|| cause.message?.match(/\b(ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH)\b/)?.[1];
return buildError(method, url, { errno, code, fetchError });
}
const { handleHttpError } = require('./errors');
/**
* Fetch with standardized error handling for mtx modules.
* Network errors are wrapped via buildNetworkError.
* HTTP errors are passed through handleHttpError which maps status codes to user-facing messages.
*
* @param {string} method - HTTP method
* @param {string} url - Request URL
* @param {object} [options] - fetch options (headers, body, etc.)
* @param {object} [params] - ParamCollection for handleHttpError context
* @returns {Promise<Response>}
*/
async function mtxFetch(method, url, options = {}, params) {
let response;
try {
response = await fetch(url, { method, ...options });
} catch (e) {
throw buildNetworkError(method, url, e);
}
if (!response.ok) {
handleHttpError(await buildResponseError(method, url, response), params, { url });
}
return response;
}
module.exports = {
buildError,
buildResponseError,
buildNetworkError,
parseResponseBody,
mtxFetch
};