Shuhei Kagawa

HTTP request timeouts in JavaScript

@2017-05-14 23:31 - JavaScript

These days I have been working on a Node.js front-end server that calls back-end APIs and renders HTML with React components. In this microservices setup, I am making sure that the server doesn't become too slow even when its dependencies have problems. So I need to set timeouts to the API calls so that the server can give up non-essential dependencies quickly and fail fast when essential dependencies are out of order.

As I started looking at timeout options carefully, I quickly found that there were many different kinds of timeouts even in the very limited field, HTTP request with JavaScript.

Node.js "http"/"https"

Let's start with the standard library of Node.js. http and https provide request() function, which makes HTTP requests.

Timeouts on http.request()

http.request() takes a timeout option. Its documentation says:

timeout <number>: A number specifying the socket timeout in milliseconds. This will set the timeout before the socket is connected.

So what does it actually do? It internally calls net.createConnection() with its timeout option, which eventually calls socket.setTimeout() before the socket starts connecting.

There is also http.ClientRequest.setTimeout(). Its documentation says:

Once a socket is assigned to this request and is connected socket.setTimeout() will be called.

So this also calls socket.setTimeout().

Either of them don't close the connection when the socket timeouts but only emits a timeout event.

So, what does socket.setTimeout() do? Let's check.

net.Socket.setTimeout()

The documentation says:

Sets the socket to timeout after timeout milliseconds of inactivity on the socket. By default net.Socket do not have a timeout.

OK, but what does "inactivity on the socket" exactly mean? In a happy path, a TCP socket follows the following steps:

  1. Start connecting
  2. DNS lookup is done: lookup event (Doesn't happen in HTTP Keep-Alive)
  3. Connection is made: connect event (Doesn't happen in HTTP Keep-Alive)
  4. Read data or write data

When you call socket.setTimeout(), a timeout timer is created and restarted before connecting, after lookup, after connect and each data read & write. So the timeout event is emitted on one of the following cases:

  • DNS lookup doesn't finish in the given timeout
  • TCP connection is not made in the given timeout after DNS lookup
  • No data read or write in the given timeout after connection, previous data read or write

This might be a bit counter-intuitive. Let's say you called socket.setTimeout(300) to set the timeout as 300 ms, and it took 100 ms for DNS lookup, 100 ms for making a connection with a remote server, 200 ms for the remote server to send response headers, 50 ms for transferring the first half of the response body and another 50 ms for the rest. While the entire request & response took more than 500 ms, timeout event is not emitted at all.

Because the timeout timer is restarted in each step, timeout happens only when a step is not completed in the given time.

Then what happens if timeouts happen in all of the steps? As far as I tried, timeout event is triggered only once.

Another concern is HTTP Keep-Alive, which reuses a socket for multiple HTTP requests. What happens if you set timeout for a socket and the socket is reused for another HTTP request? Never mind. timeout set in a HTTP request does not affect subsequent HTTP requests because the timeout is cleaned up when it's kept alive.

HTTP Keep-Alive & TCP Keep-Alive

This is not directly related to timeout, but I found Keep-Alive options in http/https are a bit confusing. They mix HTTP Keep-Alive and TCP Keep-Alive, which are completely different things but coincidentally have the same name. For example, the options of http.Agent constructor has keepAlive for HTTP Keep-Alive and keepAliveMsecs for TCP Keep-Alive.

So, how are they different?

  • HTTP Keep-Alive reuses a TCP connection for multiple HTTP requests. It saves the TCP connection overhead such as DNS lookup and TCP slow start.
  • TCP Keep-Alive closes invalid connections, and it is normally handled by OS.

So?

http/https use socket.setTimeout() whose timer is restarted in stages of socket lifecycle. It doesn't ensure a timeout for the overall request & response. If you want to make sure that a request completes in a specific time or fails, you need to prepare your own timeout solution.

Third-party modules

"request" module

request is a very popular HTTP request library that supports many convenient features on top of http/https module. Its README says:

timeout - Integer containing the number of milliseconds to wait for a server to send response headers (and start the response body) before aborting the request.

However, as far as I checked the implementation, timeout is not applied to the timing of response headers as of v2.81.1.

Currently this module emits the two types of timeout errors:

  • ESOCKETTIMEDOUT: Emitted from http.ClientRequest.setTimeout() described above, which uses socket.setTimeout().
  • ETIMEDOUT: Emitted when a connection is not established in the given timeout. It was applied to the timing of response headers before v2.76.0.

There is a GitHub issue for it, but I'm not sure if it's intended and the README is outdated, or it's a bug.

By the way, request provides a useful timing measurement feature that you can enable with time option. It will help you to define a proper timeout value.

"axios" module

axios is another popular library that uses Promise. Like request module's README, its timeout option timeouts if the response status code and headers don't arrive in the given timeout.

Browser APIs

While my initial interest was server-side HTTP requests, I become curious about browser APIs as I was investigating Node.js options.

XMLHttpRequest

XMLHttpRequest.timeout aborts a request after the given timeout and calls ontimeout event listeners. The documentation does not say about the exact timing, but I guess that it is until readyState === 4, which means that the entire response body has arrived.

fetch()

As far as I read fetch()'s documentation on MDN, it does not have any way to specify timeout. So we need to handle by ourselves. We can do that easily using Promise.race().

function withTimeout(msecs, promise) {
  const timeout = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('timeout'));
    }, msecs);
  });
  return Promise.race([timeout, promise]);
}

withTimeout(1000, fetch('https://foo.com/bar/'))
  .then(doSomething)
  .catch(handleError);

This kind of external approach works with any HTTP client and timeouts for the overall request and response. However, it does not abort the underlying HTTP request while preceding timeouts actually abort HTTP requests and save some resources.

Conclusion

Most of the HTTP request APIs in JavaScript don't offer timeout mechanism for the overall request and response. If you want to limit the maximum processing time for your piece of code, you have to prepare your own timeout solution. However, if your solution relies on high-level abstraction like Promise and cannot abort underlying TCP socket and HTTP request when timeout, it is nice to use the exiting low-level timeout mechanisms like socket.setTimeout() together to save some resources.

How to export CommonJS and ES Module

@2017-01-05 22:11 - JavaScript

After my previous post about jsnext:main and module, there came another issue.

Here is the twists and turns that I wandered to solve the problem.

Exports

The code of material-colors looked like the following.

colors.js specified in main (CommonJS version)

module.exports = {
  red: { /* ... */ },
  blue: { /* ... */ }
};

colors.es2015.js specified in jsnext:main/module (ES Module version)

export var red = { /* ... */ };
export var blue = { /* ... */ };

Then the ES Module file can get benefit of tree shaking if it's imported by named imports.

Problem of having only named exports

The colors.es2015.js broke react-color when built with Webpack 2 because it was doing default import but colors.es2015.js didn't have default export.

import material from 'material-colors';
console.log(material.red);

So @echenley suggested to change it to a wildcard import.

import * as material from 'material-colors';
console.log(material.red);

It worked well, but I removed jsnext:main and module because other libraries with default import may break on Webpack 2 and material-colors is already tiny without tree shaking anyway.

Have a default export

After a while, I came up with a better solution to have a default export in addition to named exports. Then it will work well with tree shaking and won't break default import. Pretty obvious after coming up.

export var red = { /* ... */ };
export var blue = { /* ... */ };

export default {
  red: red,
  blue: blue
};

So?

To keep maximum compatibility for CommonJS and ES Module:

  • If your CommonJS module exports only one thing, like encouraged in the npm world, export it as a default export.
  • If your CommonJS module exports multiple things, which essentially exports an object with them as properties, export named exports. In addition to it, it's safer to have a default export just in case for the problem described above.

main, jsnext:main and module

@2017-01-05 00:00 - JavaScript

Node module's package.json has main property. It's the entry point of a package, which is exported when a client requires the package.

Recently, I got an issue on one of my popular GitHub repos, material-colors. It claimed that "colors.es2015.js const not supported in older browser (Safari 9)", which looked pretty obvious to me. ES2015 is a new spec. Why do older browsers support it?

I totally forgot about it at the time, but the colors.es2015.js was exposed as the npm package's jsnext:main. And to my surprise, it turned out that jsnext:main shouldn't have jsnext or ES2015+ features like const, arrow function and class. What a contradiction!

jsnext:main

Module bundlers that utilizes tree shaking to reduce bundle size, like Rollup and Webpack 2, require packages to expose ES Modules with import and export. So they invented a non-standard property called jsnext:main.

However, it had a problem. If the file specified jsnext:main contains ES2015+ features, it won't run without transpilation on browsers that don't support those features. But normally people don't transpile packages in node_modules, and many issues were created on GitHub. To solve the problem, people concluded that jsnext:main shouldn't have ES2015+ features other than import and export. What an irony.

module

Now the name jsnext:main is too confusing. I was confused at least. People discussed for a better name, and module came out that supersedes jsnext:main. And it might be standardized.

So?

I looked into a couple of popular repos, and they had both of jsnext:main and module in addition to main.

At this time, it seems to be a good idea to have both of them if you want to support tree shaking. If you don't, just go with only the plain old main.