Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
twilio-node changelog
=====================

[Unreleased] Version 5.9.0
---------------------------
**Library - Feature**
- Replace axios with fetch for HTTP requests to reduce bundle size by 100KB+
- Added `fetch` option to RequestClientOptions to allow custom fetch implementations (undici, node-fetch, etc.)
- Maintained all existing functionality including retries, HTTPS agents, and validation
- Fallback to global fetch if no custom fetch provided (Node.js 18+ or browser environments)
- See examples/custom-fetch.md for usage examples

[2025-08-18] Version 5.8.1
--------------------------
**Library - Chore**
Expand Down
4 changes: 2 additions & 2 deletions advanced-examples/custom-http-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

If you are working with the Twilio Node.js Helper Library, and you need to modify the HTTP requests that the library makes to the Twilio servers, you’re in the right place.

The helper library uses [axios](https://www.npmjs.com/package/axios), a promise-based HTTP client, to make requests. You can also provide your own `httpClient` to customize requests as needed.
The helper library uses the native `fetch` API to make requests. You can provide your own `fetch` implementation to customize requests as needed, which helps reduce bundle size by allowing you to bring your own HTTP client.

The following example shows a typical request without a custom `httpClient`.
The following example shows a typical request without a custom `fetch` implementation.

```js
const client = require('twilio')(accountSid, authToken);
Expand Down
79 changes: 79 additions & 0 deletions examples/custom-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Bringing Your Own Fetch Implementation

With the new fetch-based Twilio Node Helper Library, you can significantly reduce bundle size by providing your own fetch implementation. This is particularly useful for:

- Edge environments that have built-in fetch
- Node.js applications that want to use undici or other optimized HTTP clients
- Lambda functions where bundle size matters

## Using Built-in Fetch (Node.js 18+)

```js
const twilio = require('twilio');

// No additional configuration needed - uses global fetch
const client = twilio(accountSid, authToken);

client.messages.create({
to: '+15555555555',
from: '+15555555551',
body: 'Using built-in fetch!'
});
```

## Using Undici for Better Performance

```js
const twilio = require('twilio');
const { fetch } = require('undici');

const client = twilio(accountSid, authToken, {
httpClient: new twilio.RequestClient({ fetch })
});

client.messages.create({
to: '+15555555555',
from: '+15555555551',
body: 'Using undici for better performance!'
});
```

## Using node-fetch for Compatibility

```js
const twilio = require('twilio');
const fetch = require('node-fetch');

const client = twilio(accountSid, authToken, {
httpClient: new twilio.RequestClient({ fetch })
});
```

## Custom Fetch with Additional Features

```js
const twilio = require('twilio');

// Custom fetch wrapper with logging
const customFetch = async (url, options) => {
console.log(`Making request to: ${url}`);
const response = await fetch(url, options);
console.log(`Response status: ${response.status}`);
return response;
};

const client = twilio(accountSid, authToken, {
httpClient: new twilio.RequestClient({
fetch: customFetch,
autoRetry: true,
maxRetries: 5
})
});
```

## Benefits

- **Reduced Bundle Size**: No axios dependency (~100KB+ saved)
- **Better Performance**: Use optimized HTTP clients like undici
- **Environment Flexibility**: Works in Edge, Node.js, and browser environments
- **Future-Proof**: Uses web standards instead of library-specific APIs
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"url": "https://github.com/twilio/twilio-node.git"
},
"dependencies": {
"axios": "^1.11.0",
"dayjs": "^1.11.9",
"https-proxy-agent": "^5.0.0",
"jsonwebtoken": "^9.0.2",
Expand Down
146 changes: 43 additions & 103 deletions spec/unit/base/RequestClient.spec.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
import mockfs from "mock-fs";
import axios from "axios";
import RequestClient from "../../../src/base/RequestClient";
import HttpsProxyAgent from "https-proxy-agent";
import http from "http";

function createMockAxios(promiseHandler) {
let instance = function () {
return promiseHandler;
};
// Global fetch mock setup
global.fetch = jest.fn();

instance.defaults = {
headers: {
post: {},
},
};

return instance;
function createMockFetch(responsePromise) {
return jest.fn(() => responsePromise);
}

describe("RequestClient constructor", function () {
let createSpy;
let fetchSpy;
let initialHttpProxyValue = process.env.HTTP_PROXY;

beforeEach(function () {
createSpy = jest.spyOn(axios, "create");
createSpy.mockReturnValue(
createMockAxios(
Promise.resolve({
status: 200,
data: "voltron",
headers: { response: "header" },
})
)
);
fetchSpy = global.fetch;
fetchSpy.mockResolvedValue({
status: 200,
text: () => Promise.resolve("voltron"),
headers: new Map([["response", "header"]]),
});
});

afterEach(function () {
createSpy.mockRestore();
fetchSpy.mockRestore();
if (initialHttpProxyValue) {
process.env.HTTP_PROXY = initialHttpProxyValue;
} else {
Expand All @@ -47,48 +35,26 @@ describe("RequestClient constructor", function () {
it("should initialize with default values", function () {
const requestClient = new RequestClient();
expect(requestClient.defaultTimeout).toEqual(30000);
expect(requestClient.axios.defaults.headers.post).toEqual({
"Content-Type": "application/x-www-form-urlencoded",
});
expect(requestClient.axios.defaults.httpsAgent).not.toBeInstanceOf(
HttpsProxyAgent
);
expect(requestClient.axios.defaults.httpsAgent.options.timeout).toEqual(
30000
);
expect(requestClient.axios.defaults.httpsAgent.options.keepAlive).toBe(
true
);
expect(requestClient.axios.defaults.httpsAgent.options.keepAliveMsecs).toBe(
undefined
);
expect(requestClient.axios.defaults.httpsAgent.options.maxSockets).toBe(20);
expect(
requestClient.axios.defaults.httpsAgent.options.maxTotalSockets
).toBe(100);
expect(requestClient.axios.defaults.httpsAgent.options.maxFreeSockets).toBe(
5
);
expect(requestClient.axios.defaults.httpsAgent.options.scheduling).toBe(
undefined
);
expect(requestClient.axios.defaults.httpsAgent.options.ca).toBe(undefined);
expect(requestClient.fetch).toBeDefined();
expect(requestClient.agent).not.toBeInstanceOf(HttpsProxyAgent);
expect(requestClient.agent.options.timeout).toEqual(30000);
expect(requestClient.agent.options.keepAlive).toBe(true);
expect(requestClient.agent.options.keepAliveMsecs).toBe(undefined);
expect(requestClient.agent.options.maxSockets).toBe(20);
expect(requestClient.agent.options.maxTotalSockets).toBe(100);
expect(requestClient.agent.options.maxFreeSockets).toBe(5);
expect(requestClient.agent.options.scheduling).toBe(undefined);
expect(requestClient.agent.options.ca).toBe(undefined);
});

it("should initialize with a proxy", function () {
process.env.HTTP_PROXY = "http://example.com:8080";

const requestClient = new RequestClient();
expect(requestClient.defaultTimeout).toEqual(30000);
expect(requestClient.axios.defaults.headers.post).toEqual({
"Content-Type": "application/x-www-form-urlencoded",
});
expect(requestClient.axios.defaults.httpsAgent).toBeInstanceOf(
HttpsProxyAgent
);
expect(requestClient.axios.defaults.httpsAgent.proxy.host).toEqual(
"example.com"
);
expect(requestClient.fetch).toBeDefined();
expect(requestClient.agent).toBeInstanceOf(HttpsProxyAgent);
expect(requestClient.agent.proxy.host).toEqual("example.com");
});

it("should initialize custom https settings (all settings customized)", function () {
Expand All @@ -102,33 +68,15 @@ describe("RequestClient constructor", function () {
scheduling: "fifo",
});
expect(requestClient.defaultTimeout).toEqual(5000);
expect(requestClient.axios.defaults.headers.post).toEqual({
"Content-Type": "application/x-www-form-urlencoded",
});
expect(requestClient.axios.defaults.httpsAgent).not.toBeInstanceOf(
HttpsProxyAgent
);
expect(requestClient.axios.defaults.httpsAgent.options.timeout).toEqual(
5000
);
expect(requestClient.axios.defaults.httpsAgent.options.keepAlive).toBe(
true
);
expect(
requestClient.axios.defaults.httpsAgent.options.keepAliveMsecs
).toEqual(1500);
expect(requestClient.axios.defaults.httpsAgent.options.maxSockets).toEqual(
100
);
expect(
requestClient.axios.defaults.httpsAgent.options.maxTotalSockets
).toEqual(1000);
expect(
requestClient.axios.defaults.httpsAgent.options.maxFreeSockets
).toEqual(10);
expect(requestClient.axios.defaults.httpsAgent.options.scheduling).toEqual(
"fifo"
);
expect(requestClient.fetch).toBeDefined();
expect(requestClient.agent).not.toBeInstanceOf(HttpsProxyAgent);
expect(requestClient.agent.options.timeout).toEqual(5000);
expect(requestClient.agent.options.keepAlive).toBe(true);
expect(requestClient.agent.options.keepAliveMsecs).toEqual(1500);
expect(requestClient.agent.options.maxSockets).toEqual(100);
expect(requestClient.agent.options.maxTotalSockets).toEqual(1000);
expect(requestClient.agent.options.maxFreeSockets).toEqual(10);
expect(requestClient.agent.options.scheduling).toEqual("fifo");
});

it("should initialize custom https settings (some settings customized)", function () {
Expand Down Expand Up @@ -158,31 +106,23 @@ describe("RequestClient constructor", function () {
expect(
requestClient.axios.defaults.httpsAgent.options.maxTotalSockets
).toEqual(1500);
expect(requestClient.axios.defaults.httpsAgent.options.maxFreeSockets).toBe(
5
);
expect(requestClient.axios.defaults.httpsAgent.options.scheduling).toEqual(
"lifo"
);
expect(requestClient.agent.options.maxFreeSockets).toBe(5);
expect(requestClient.agent.options.scheduling).toEqual("lifo");
});
});

describe("lastResponse and lastRequest defined", function () {
let createSpy;
let fetchSpy;
let client;
let response;

beforeEach(function () {
createSpy = jest.spyOn(axios, "create");
createSpy.mockReturnValue(
createMockAxios(
Promise.resolve({
status: 200,
data: "voltron",
headers: { response: "header" },
})
)
);
fetchSpy = global.fetch;
fetchSpy.mockResolvedValue({
status: 200,
text: () => Promise.resolve("voltron"),
headers: new Map([["response", "header"]]),
});

client = new RequestClient();

Expand Down
Loading
Loading