Skip to content

feat: migrate from axios to fetch#2591

Merged
WilliamBergamin merged 45 commits into
v8from
mirgate-from-axios-to-fetch
May 12, 2026
Merged

feat: migrate from axios to fetch#2591
WilliamBergamin merged 45 commits into
v8from
mirgate-from-axios-to-fetch

Conversation

@WilliamBergamin
Copy link
Copy Markdown
Contributor

@WilliamBergamin WilliamBergamin commented May 7, 2026

Summary

These changes migrates this project axios to the native Fetch API — a major step toward v8.

What changed

  • @slack/web-api — Removed axios, form-data, is-electron, and is-stream. HTTP requests now use globalThis.fetch (or a user-supplied fetch function). The public API surface is simpler: a single fetch option replaces agent, tls, requestInterceptor, and adapter.
  • @slack/webhook — Same migration: axios removed, replaced with native fetch.
  • @slack/socket-mode — Replaced the ws WebSocket library with undici's spec-compliant WHATWG WebSocket. A new dispatcher option enables proxy and custom TLS configuration. undici is declared as a peer dependency.

Breaking changes

Removed option Replacement
agent (http/https Agent) fetch option with a custom dispatcher
tls (pfx, cert, ca, etc.) fetch option with custom TLS config
requestInterceptor Wrap the fetch function
adapter Supply a custom fetch
attachOriginalToWebAPIRequestError Always attached now
httpAgent (socket-mode) dispatcher option (undici Dispatcher)
  • WebAPIHTTPError.headers type changed from IncomingHttpHeaders to Record<string, string>
  • Minimum Node.js version 20

Proxy configuration examples

Web API — proxy via custom fetch

const { ProxyAgent, fetch: undiciFetch } = require('undici');
const { WebClient } = require('@slack/web-api');

const dispatcher = new ProxyAgent('http://my-proxy:8080');

const client = new WebClient(process.env.SLACK_BOT_TOKEN, {
  fetch: (url, init) => undiciFetch(url, { ...init, dispatcher }),
});

Socket Mode — proxy via dispatcher

const { SocketModeClient, LogLevel } = require('@slack/socket-mode');
const { ProxyAgent } = require('undici');

const dispatcher = new ProxyAgent('http://my-proxy:8080');

const socketModeClient = new SocketModeClient({
  appToken: process.env.SLACK_APP_TOKEN,
  logLevel: LogLevel.DEBUG,
  // dispatcher is used for BOTH the WebSocket connection
  // AND the HTTP API calls (unless clientOptions.fetch is set)
  dispatcher,
});

Socket Mode — separate proxy for HTTP vs WebSocket

const { SocketModeClient } = require('@slack/socket-mode');
const { ProxyAgent, fetch: undiciFetch } = require('undici');

const wsProxy = new ProxyAgent('http://ws-proxy:8080');
const httpProxy = new ProxyAgent('http://http-proxy:9090');

const socketModeClient = new SocketModeClient({
  appToken: process.env.SLACK_APP_TOKEN,
  // WebSocket connections use this dispatcher
  dispatcher: wsProxy,
  clientOptions: {
    // HTTP API calls use this separate fetch
    fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: httpProxy }),
  },
});

Webhook — proxy via custom fetch

const { IncomingWebhook } = require('@slack/webhook');
const { ProxyAgent, fetch: undiciFetch } = require('undici');

const dispatcher = new ProxyAgent('http://my-proxy:8080');

const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL, {
  fetch: (url, init) => undiciFetch(url, { ...init, dispatcher }),
});

Custom fetch — override for testing or instrumentation

const { WebClient } = require('@slack/web-api');

const client = new WebClient(process.env.SLACK_BOT_TOKEN, {
  fetch: async (url, init) => {
    console.log(`→ ${init?.method ?? 'GET'} ${url}`);
    const start = Date.now();
    const response = await globalThis.fetch(url, init);
    console.log(`← ${response.status} (${Date.now() - start}ms)`);
    return response;
  },
});

Testing

Requirements

@WilliamBergamin WilliamBergamin changed the title feat!: migrate from axios to fetch (v8) feat: migrate from axios to fetch May 11, 2026
Copy link
Copy Markdown
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilliamBergamin LGTM! This is a fantastic enhancement 🪄 🎩 ✨

I'm leaving a few quibbles and thoughts of small improvements but am so excited for this PR 🏆

  • Spacing: I ask for a linebreak after imports
  • Documentation: A few inline variables weren't so clear to me at a glance and comments might be nice?
  • Slashes: Prefixed methods with "/" might guard against absurd case but brings peace of mind 😉

Let's count this toward #2359 too and I have an open question now about removing the @slack/rtm-api package and might be curious about deprecation as well - seeing the code change is motivating!

Comment on lines +158 to +166
this.messageHandler = (event: Event) => {
if (!(event instanceof MessageEvent)) {
this.logger.warn(`Expected MessageEvent but received ${event.constructor.name} (type: ${event.type})`);
return;
}
const isBinary = typeof event.data !== 'string';
this.options.client.emit('ws_message', event.data, isBinary);
};
this.websocket.addEventListener('message', this.messageHandler);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 praise: Solid patterns of the event parameter as a single argument!

// Note that ws' `autoPong` option is true by default, so no need to respond to ping.
// see https://github.com/websockets/ws/blob/2aa0405a5e96754b296fef6bd6ebdfb2f11967fc/doc/ws.md#new-websocketaddress-protocols-options
// Subscribe to undici diagnostics_channel for WebSocket ping/pong frame events.
// These channels fire for ALL undici WebSocket instances, so we filter by matching instance.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 praise: Ouch! Thanks for adding this note! Am I understanding that messages cross between processes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yess thanks for pointing this out, I want to see if I can improve this logic slightly but I don't think I can 🤔 currently if you have more then one socket connection then all the ping messages will be received by the pingHandler then it is responsible for filtering relevant ping messages

Comment on lines -249 to +324
this.websocket?.ping(pingMessage);
if (!this.websocket) {
this.logger.error('WebSocket not available, skipping ping.');
return;
}
ping(this.websocket, Buffer.from(pingMessage));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌪️ thought: Fascinating change of what's attached to this websocket!

Comment thread packages/socket-mode/examples/proxy.js
// const webClient = new WebClient(process.env.SLACK_BOT_TOKEN, {
// logLevel: LogLevel.DEBUG,
// clientOptions,
// fetch: (url, options) => fetch(url, { ...options, dispatcher })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 praise: Big fan of how this can be reused!

*/
private dispatcher?: SocketModeDispatcher;

private connectionResponse?: AppsConnectionsOpenResponse;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📚 quibble: A kind request for @jsdoc for what this goes toward might give context to later debugging!

Comment on lines +132 to +135
if (dispatcher && this.webClientOptions.fetch === undefined) {
this.webClientOptions.fetch = (url, init) =>
undiciFetch(url, { ...init, dispatcher: dispatcher as Dispatcher } as RequestInit);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌟 praise: Quite a delightful pattern to find with shared unidici defaults!

*/
export interface SocketModeDispatcher {
// biome-ignore lint/suspicious/noExplicitAny: structural compatibility with any undici Dispatcher version
dispatch(options: any, handler: any): boolean;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📚 question: The expected options and handler values aren't clear to me and I'm wondering if documenting these is still useful alongside the "any" option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question maybe adding more @jsdoc make more sense 💯

This is a structural type, its main goal is to prevent our project from directly exposing undici types, this provides users with more flexibility when definingDispatchers and gives use more leeway when the time comes to bump undici version

Comment thread packages/web-api/src/WebClient.ts Outdated
Comment thread packages/web-api/src/WebClient.ts Outdated
WilliamBergamin and others added 8 commits May 12, 2026 12:01
Co-authored-by: Eden Zimbelman <eden.zimbelman@salesforce.com>
Co-authored-by: Eden Zimbelman <eden.zimbelman@salesforce.com>
Co-authored-by: Eden Zimbelman <eden.zimbelman@salesforce.com>
Copy link
Copy Markdown
Contributor

@srtaalej srtaalej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ⭐ ⭐ ⭐

Comment thread packages/web-api/src/WebClient.ts Outdated
@srtaalej srtaalej added dependencies Pull requests that update a dependency file semver:major pkg:web-api applies to `@slack/web-api` pkg:webhook applies to `@slack/webhook` pkg:socket-mode applies to `@slack/socket-mode` labels May 12, 2026
WilliamBergamin and others added 2 commits May 12, 2026 15:39
Co-authored-by: Ale Mercado <104795114+srtaalej@users.noreply.github.com>
@WilliamBergamin WilliamBergamin merged commit 852a20c into v8 May 12, 2026
10 checks passed
@WilliamBergamin WilliamBergamin deleted the mirgate-from-axios-to-fetch branch May 12, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file pkg:socket-mode applies to `@slack/socket-mode` pkg:web-api applies to `@slack/web-api` pkg:webhook applies to `@slack/webhook` semver:major

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants