UNPKG

@nodescript/stdlib

Version:
182 lines (181 loc) 6.03 kB
import { determineRequestBody, FetchError, FetchResponseType, headersToObject, HttpRequestFailed, mergeUrlQuery, readResponse, } from '../lib/web.js'; export const module = { version: '2.2.0', moduleName: 'Web / Http Request', description: ` Sends an HTTP request using backend-powered HTTP client. The request is not subject to CORS limitations of the browser and can be used to specify and access arbitrary request/response headders. `, keywords: ['fetch'], params: { method: { schema: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], default: 'GET', }, }, url: { schema: { type: 'string' }, }, query: { schema: { type: 'object', properties: {}, additionalProperties: { type: 'string', optional: true }, }, advanced: true, }, headers: { schema: { type: 'object', properties: {}, additionalProperties: { type: 'string', optional: true }, }, advanced: true, }, body: { schema: { type: 'any' }, hideValue: true, advanced: true, }, responseType: { schema: { type: 'string', enum: Object.values(FetchResponseType), default: FetchResponseType.AUTO, }, advanced: true, }, followRedirects: { schema: { type: 'boolean', default: true }, advanced: true, }, proxyUrl: { schema: { type: 'string' }, advanced: true, }, throw: { schema: { type: 'boolean', default: true }, advanced: true, }, retries: { schema: { type: 'number', default: 1 }, advanced: true, }, ca: { schema: { type: 'string', optional: true }, advanced: true, }, }, result: { async: true, schema: { type: 'object', properties: { status: { type: 'number' }, url: { type: 'string' }, headers: { type: 'object', additionalProperties: { type: 'any' }, }, body: { type: 'any' }, } }, }, cacheMode: 'always', evalMode: 'manual', }; export const compute = async (params, ctx) => { const { url, retries } = params; if (!url.trim()) { // Do not send requests to self by default return undefined; } if (!/^https?:\/\//.test(url)) { throw new Error('URL must start with http:// or https://'); } const maxAttempts = 1 + (Math.min(Math.max(retries, 0), 10) || 0); let lastError = null; let delay = 500; for (let attempt = 0; attempt < maxAttempts; attempt += 1) { try { return await sendSingle(params, ctx); } catch (error) { lastError = error; // Retry only on 5xx and connection errors if (error.status >= 500) { await new Promise(r => setTimeout(r, delay)); delay = Math.max(delay * 2, 5000); } else { break; } } } throw lastError; }; async function sendSingle(params, ctx) { const { method, url, headers, body, responseType = FetchResponseType.AUTO, } = params; const actualHeaders = prepHeaders(headers); const [actualBody, contentType] = determineRequestBody(method, body); if (contentType && !actualHeaders['content-type']) { actualHeaders['content-type'] = contentType; } const fetchServiceUrl = getAdapterUrl(params, ctx); const res = await fetch(fetchServiceUrl + '/request', { method: 'POST', headers: makeControlHeaders(params, actualHeaders), body: actualBody, }); if (!res.ok) { const responseBodyText = await res.text(); const message = (ctx.lib.parseJson(responseBodyText, {})).message ?? responseBodyText; throw new FetchError(res.status, message); } const status = Number(res.headers.get('x-fetch-status')) || 0; const responseHeaders = ctx.lib.parseJson(res.headers.get('x-fetch-headers') ?? '{}', {}); const isErrorStatus = status === 0 || status >= 400; if (params.throw && isErrorStatus) { const responseBodyText = await res.text(); const details = ctx.lib.parseJson(responseBodyText) ?? { response: responseBodyText }; throw new HttpRequestFailed(status, method, url, details); } const resContentType = responseHeaders['content-type'] ?? 'text/plain'; return { status, headers: headersToObject(responseHeaders), body: await readResponse(res, responseType, resContentType), }; } function makeControlHeaders(params, requestHeaders) { const { method, url, query, proxyUrl, followRedirects, ca, } = params; const actualUrl = mergeUrlQuery(url, query); const connectOptions = {}; if (ca) { connectOptions.ca = ca; } return { 'x-fetch-method': method, 'x-fetch-url': actualUrl, 'x-fetch-headers': JSON.stringify(requestHeaders), 'x-fetch-follow-redirects': String(followRedirects), 'x-fetch-proxy': proxyUrl.trim(), 'x-fetch-connect-options': JSON.stringify(connectOptions), }; } function prepHeaders(headers) { const result = {}; for (const [key, value] of Object.entries(headers)) { if (value === undefined) { continue; } result[key] = String(value); } return result; } function getAdapterUrl(params, ctx) { return ctx.getLocal('FETCH_SERVICE_URL') ?? 'https://fetch.nodescript.dev'; }