← Back to Blog

Case Study: 134 Unguarded Axios Calls Across 35 Integrations in botpress/botpress

By Nark Team

botpress/botpress is an open-source bot framework with 12,000+ GitHub stars. Their integrations/ directory contains 35+ third-party service integrations — Vonage, Viber, Zendesk (Sunco), Google Analytics, Slack, HubSpot, Airtable, Notion, Stripe, and more. We scanned the codebase with Nark and found 134 unguarded axios calls spread across these integrations. Every one of them crashes the bot handler on any HTTP failure — network timeout, expired credentials, rate limit, server error — surfacing a raw AxiosError stack trace instead of a meaningful error.


The Finding

The dominant pattern across botpress integrations is a bare axios.post() or axios.get() with no try-catch:

export async function sendMessage({ conversation, ctx, ack }, payload) {
  const { to, from, channel } = getRequestMetadata(conversation);
  // No try-catch — 401, 429, timeout, or network failure crashes here
  const response = await axios.post(
    'https://api.nexmo.com/v1/messages',
    { ...payload, from, to, channel },
    { auth: {
        username: ctx.configuration.apiKey,
        password: ctx.configuration.apiSecret
    }}
  );
  await ack({ tags: { id: response.data.message_uuid } });
}

Axios throws an AxiosError on any non-2xx response by default. This is not a bug in axios — it is documented behavior. But when the caller does not catch the error, the failure propagates as an unhandled rejection that crashes the bot handler.


Why This Matters

Bot handlers crash on transient failures. A Vonage 429 (rate limit) or a Viber 503 (service degradation) kills the handler instead of producing a retryable error. The bot user sees nothing — the message silently fails.

Raw stack traces replace useful errors. Botpress has a RuntimeError class (from @botpress/sdk and @botpress/client) designed for surfacing human-readable errors. Without a try-catch, the raw AxiosError with a full HTTP response dump reaches the user instead.

The analytics ping was re-throwing. The Google Analytics integration has a _collect() method — a fire-and-forget telemetry ping. It was making a bare axios.post() that would re-throw on failure. A Google Analytics outage would crash bot handlers processing real user messages. Telemetry calls should never crash the caller.


Specific Integrations Affected

We examined 4 integrations in detail as a representative sample:

Vonage (integrations/vonage/src/vonage.ts)

sendMessage makes a bare axios.post to the Vonage Messages API. If the API returns 401 (expired credentials) or 429 (rate limit), the raw AxiosError crashes the handler.

The fix: Wrap in try-catch, check axios.isAxiosError(), re-throw as RuntimeError with the HTTP status and message.

try {
  const response = await axios.post('https://api.nexmo.com/v1/messages', payload, { auth });
  await ack({ tags: { id: response.data.message_uuid } });
} catch (error) {
  if (axios.isAxiosError(error)) {
    const status = error.response?.status ?? 'network error';
    throw new RuntimeError(`Failed to send Vonage message (${status}): ${error.message}`);
  }
  throw error;
}

Viber (integrations/viber/src/index.ts)

Two bare axios calls: setViberWebhook (called during register/unregister) and sendViberMessage (called by every channel message handler). A network failure in setViberWebhook silently prevents webhook setup with no diagnostic information.

Zendesk Sunco (integrations/sunco/src/client.ts)

Three unguarded calls in SuncoClient: listIntegrations and two inside downloadAndUploadAttachment (one to download a source file, one to upload via postForm). A 404 on the source URL or a 413 from Zendesk crashes the attachment pipeline.

Google Analytics (integrations/google-analytics/src/client.ts)

Two unguarded calls. validateConfiguration calls the debug endpoint — a network failure during bot setup produces an unhelpful crash. _collect is the fire-and-forget telemetry method — it should log and continue on failure, never re-throw.

The existing code already has a parseError helper that handles isAxiosError — but none of the methods use it.


The RuntimeError Inconsistency

Botpress integrations use RuntimeError from @botpress/client or @botpress/sdk for structured error reporting. But usage is inconsistent:

  • Some integrations import RuntimeError but never use it in catch blocks
  • Some integrations throw plain Error instead of RuntimeError
  • The Google Analytics integration already has a parseError helper for axios errors but calls it in zero methods

The fix is mechanical: wrap bare axios calls in try-catch, check axios.isAxiosError(), and throw RuntimeError with the HTTP status and message. For fire-and-forget calls like _collect, log instead of throwing.


Scale

134 unguarded calls across 35 integrations. The 4 integrations above contain 8 of the 134. The remaining 126 follow the same pattern — a bare axios.get() or axios.post() to an external API with no error handling.

A single PR fixing 4 integrations demonstrates the approach. If adopted, the remaining integrations can be fixed in follow-up PRs using the same mechanical pattern.


Broader Context

From our bulk scan of 6,283 TypeScript repos: 409 repos use axios. 378 have violations (92%). 4,132 total unguarded calls.

92% means this is the default state. Most TypeScript projects that use axios have at least one call that crashes when the remote server returns a non-2xx status. Botpress is not uniquely careless — they are representative of the ecosystem. The difference is that in a bot framework, a crashed handler means a user's message disappears silently.


Source Data

  • Scan tool: Nark (Profile: axios)
  • Repository: botpress/botpress on GitHub
  • Scan date: April 2026
  • Total axios violations in repo: 134 across 35 integrations
  • Bulk scan aggregate: 6,283 repos scanned, 409 use axios, 92% have at least one unguarded call