UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

159 lines (144 loc) 5.23 kB
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 };