@nodescript/stdlib
Version:
Standard Node Definitions
182 lines (181 loc) • 6.03 kB
JavaScript
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';
}