External API Patterns
Handle HTTP responses, retries, timeouts, and concurrency in plugins.
Use this guide when a plugin calls an HTTP service, such as an LLM or CRM. It covers response handling, timeouts, retries, and bounded concurrency.
Making HTTP Requests
Use fetch
Node.js 22+ includes native fetch:
events.on('message.received', async ({ message }) => {
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify({ query: message.body }),
});
const result = await response.json();
});Adding Peer Dependencies
If you need axios or other HTTP libraries, add them as peer dependencies:
{
"peerDependencies": {
"axios": ">=1.0.0"
}
}Users install them separately:
npm install axiosError Handling
HTTP Status Codes
try {
const response = await fetch(url);
if (response.status === 429) {
// Rate limited
const retryAfter = response.headers.get('Retry-After');
logger.warn('Rate limited', { retryAfter });
return; // Skip this message. Add a retry queue if it must be retried.
}
if (response.status === 401) {
logger.error('Invalid API key');
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
} catch (error) {
logger.error('API call failed', { error: error.message });
}Timeout a request
try {
const response = await fetch(url, { signal: AbortSignal.timeout(10000) });
// Process response
} catch (error) {
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
logger.warn('Request timed out');
} else {
logger.error('Request failed', { error: error.message });
}
}Retry with Exponential Backoff
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
if (attempt === maxRetries) return response;
logger.warn('Rate limited, retrying', { attempt, delay });
await new Promise(r => setTimeout(r, delay));
continue;
}
return response; // Non-429 error, return it
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
logger.warn('Retrying', { attempt, delay });
await new Promise(r => setTimeout(r, delay));
}
}
}Choose how the handler waits
Handle work asynchronously
An awaited network request keeps this handler pending. If it should return before the request completes, start the work separately and handle its rejection:
// Waits for the request before this hook completes.
events.on('message.received', async ({ message }) => {
const result = await slowAPI(message.body);
await client.sendText(message.from, result);
});
// Returns without waiting for the request; rejection is logged.
events.on('message.received', async ({ message }) => {
processMessage(message).catch(error => {
logger.error('Processing failed', { error: error.message });
});
});Queue-Based Processing
Use a queue to control concurrency:
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 3 });
events.on('message.received', async ({ message }) => {
await queue.add(async () => {
const result = await callAPI(message.body);
await client.sendText(message.from, result);
}).catch(error => {
logger.error('Queued request failed', { error: error.message });
});
});Bound how long you wait
Pass a deadline to the external client when it supports cancellation. AbortSignal.timeout() stops waiting after five seconds; the service must honor the signal to cancel the request:
events.on('message.received', async ({ message }) => {
try {
const result = await callAPI(message.body, { signal: AbortSignal.timeout(5000) });
await client.sendText(message.from, result);
} catch (error) {
logger.warn('API call skipped', { reason: error.message });
}
});Performance
Concurrent Call Limits
Set reasonable concurrency limits:
const queue = new PQueue({
concurrency: 5, // Maximum 5 concurrent API calls
});Caching
Cache API responses for identical requests:
const cache = new Map<string, { data: unknown; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getCached(key: string) {
const entry = cache.get(key);
if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
return entry.data;
}
return null;
}
async function setCached(key: string, data: unknown) {
cache.set(key, { data, timestamp: Date.now() });
}Memory Management
Clear caches periodically:
// Remove expired entries once a minute.
setInterval(() => {
const now = Date.now();
for (const [key, entry] of cache.entries()) {
if (now - entry.timestamp > CACHE_TTL) {
cache.delete(key);
}
}
}, 60 * 1000);Related
- Plugin getting started - Build your first plugin
- Plugin security model - Security boundaries
- Hooks reference - Available hooks
Was this helpful?
Your answer includes the page path and docs version.
