This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.
You can replace the built-in fetch with any compatible HTTP client via
AppContext.httpClient. This lets you add retry logic, timeouts, tracing, mocks in tests and so on.
import { Bot } from 'umbot';
import { fullPlatforms } from 'umbot/plugins';
const bot = new Bot();
bot.use(fullPlatforms);
const ctx = bot.getAppContext();
ctx.httpClient = async (input, init) => {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(input, { ...init, signal: controller.signal });
clearTimeout(id);
return res;
} catch (e) {
clearTimeout(id);
throw e;
}
};
bot.start('localhost', 3000);
import { Bot } from 'umbot';
import { fullPlatforms } from 'umbot/plugins';
const bot = new Bot();
bot.use(fullPlatforms);
const ctx = bot.getAppContext();
ctx.httpClient = async (input, init) => {
const maxRetries = 3;
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(input, init);
if (res.ok) return res;
lastError = new Error(`HTTP ${res.status}`);
} catch (e) {
lastError = e as Error;
}
// Exponential backoff before retrying
if (attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * 2 ** (attempt - 1)));
}
}
throw lastError;
};
bot.start('localhost', 3000);
import { Bot } from 'umbot';
import { fullPlatforms } from 'umbot/plugins';
const bot = new Bot();
bot.use(fullPlatforms);
const ctx = bot.getAppContext();
ctx.httpClient = async (input, init) => {
const start = performance.now();
// input can be string | URL | Request — narrow the type
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
console.log(`[HTTP] → ${init?.method ?? 'GET'} ${url}`);
try {
const res = await fetch(input, init);
const ms = (performance.now() - start).toFixed(1);
console.log(`[HTTP] ← ${res.status} ${url} (${ms}ms)`);
return res;
} catch (e) {
const ms = (performance.now() - start).toFixed(1);
console.log(`[HTTP] ✗ ${url} ERROR (${ms}ms): ${e}`);
throw e;
}
};
bot.start('localhost', 3000);
import { BotTest } from 'umbot/test';
import { fullPlatforms } from 'umbot/plugins';
const bot = new BotTest();
bot.use(fullPlatforms);
const ctx = bot.getAppContext();
// Replace fetch with a mock
ctx.httpClient = async (input, init) => {
// input can be string | URL | Request — narrow the type
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
if (url.includes('api.weather.com')) {
return new Response(JSON.stringify({ temp: 25 }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
};
// Run through simulate() — unlike the interactive bot.test(),
// it does not block waiting for console input (neither of them sends real requests
// to platform APIs — both set skipAutoReply)
await bot.simulate('what is the weather today?');
Request.send() does not throw: on error it returns { status: false, data: null, err }. If the server
responded but not with 2xx, the result also contains httpStatus and errorBody (the response body, up to 1000 characters). They let you
tell "retry later" (429) from a request error (400). On timeouts and network errors these fields are absent.
const res = await request.send('https://api.example.com/method');
if (!res.status && res.httpStatus === 429) {
// res.errorBody — the reason for the refusal and the retry parameters from the API
}
TelegramRequest uses this itself: on a 429 with retry_after up to 5 seconds, the request is retried once with the same
body. A longer pause (for example, the limit of 20 messages per minute in groups) cannot be awaited inside a webhook response —
such a message is not sent, and the reason is written to the log.
Request.signal)Besides the maxTimeQuery timeout, Request has a signal field — an external cancellation signal. Both work together:
the request is aborted by whichever fires first. Like the request body, signal belongs to a single call and
is reset after send(), and the request's subscription to the external signal is removed once it finishes — a long-lived
signal (a long polling session) does not accumulate subscriptions. This is how the built-in adapters abort a long polling request on
bot.stopPolling().
const controller = new AbortController();
const request = new Request(appContext);
request.maxTimeQuery = 35_000;
request.signal = controller.signal;
const pending = request.send('https://api.example.com/updates');
controller.abort(); // send() returns { status: false, err } without waiting for the timeout
httpClient must match this signature:
type THttpClient = (url: URL | RequestInfo, init?: RequestInit) => Promise<Response>;
It is compatible with the global fetch in Node.js 20.19+, as well as with libraries such as node-fetch, undici, got (via a wrapper).
Full reference — API v-3.1 · all versions.