Home | Send Feedback

ky - elegant fetch

Published: 10. August 2019  •  javascript

In this article, we are going to take a closer look at ky, a JavaScript HTTP client library for the browser on top of the Fetch API. ky adds convenience functions and new features to the existing Fetch API feature set.

ky targets modern browsers and depends on the Fetch API. If you want to use ky on older browsers you may need to add the Fetch polyfill

You add ky to an npm managed project with npm install ky.

In the following article, I'm going to show you examples of all new features and convenience functions of ky.

Basics

Let's first look at a fetch example that sends a GET request to a server to get a text response.

  let response = await fetch('http://localhost:8080/simple-get');
  let body = await response.text();

main.js

If we do the same with ky we write the following code.

  response = await ky('http://localhost:8080/simple-get');
  body = await response.text();

main.js

ky accepts the same parameters as fetch. The first parameter is either a URL string or a Request object, and the second parameter is an object containing custom settings. The return value in both cases is a Promise that resolves to a Response object.

This does not look very exciting; it's almost the same code; we only replaced fetch with ky. However, under the hood, there are a few changes.

We are going to look at all these changes in a bit more detail later in this blog post.


Both, fetch() and ky() return a Promise that resolves to a Response object. To extract the body an application has to call one of these methods: text(), json(), formData(), blob() and arrayBuffer(). These methods also run asynchronously and return a Promise, so we have to wait two times, first on the response and then on the body extraction.

ky simplifies this process by exposing these 5 body methods through the Response Promise. With this improvement, we can rewrite the example from above in one line and only have to wait once.

body = await ky.get('http://localhost:8080/simple-get').text();

This is not only syntax sugar; it also has a practical side effect. ky automatically sets the proper Accept header in the request.

Notice that these methods overwrite a custom Accept header. The following example sends Accept: */* instead of the header, we configured.

body = await ky.get('http://localhost:8080/simple-get', { headers: { Accept: 'application/octet-stream' } }).arrayBuffer();

If you want to send a custom Accept request header, you must use the long-form and await twice.

  response = await ky.get('http://localhost:8080/simple-get', { headers: { Accept: 'application/octet-stream' } });
  body = await response.arrayBuffer();

main.js

Method shortcuts

When you want to send a request other than GET with the Fetch API, you have to specify the method option.

fetch('....', { method: 'POST', ...});
fetch('....', { method: 'PUT', ...});

ky supports this as well

ky('....', { method: 'POST', ...});
ky('....', { method: 'PUT', ...});

It's a bit cumbersome to always specify the method, therefore ky introduces shortcut methods, to make this a bit more convenient.

ky.get(input, [options])
ky.post(input, [options])
ky.put(input, [options])
ky.patch(input, [options])
ky.head(input, [options])
ky.delete(input, [options])

All these methods assign the proper value to the method option ('GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE').

Notice that ky.get(...) and ky(...) are equivalent, because 'GET' is the default value for the method option. In my humble opinion ky.get makes the code a bit more readable, because you don't have to know that GET is the default.

Posting data

Posting JSON data with fetch requires us to write a few lines of code. We have to specify the Content-Type header and the method option, and we have to convert the object we want to send to a JSON string. If we expect a JSON response, we then have to check if the response was okay and extract the response body.

  let response = await fetch('http://localhost:8080/simple-post', {
    method: 'POST',
    body: JSON.stringify({ value: "hello world" }),
    headers: {
      'content-type': 'application/json'
    }
  });

  if (response.ok) {
    const body = await response.json();
    console.log(body);
  }

main.js

With ky this becomes a one-liner. Thanks to the post() shortcut method, we can omit the method option. With the json option, a new option introduced by ky, we can directly pass the object, without converting it to a JSON string first. ky internally converts the object with stringify and assigns the string to the body option. ky also implicitly sets the Content-Type header to application/json. Lastly, we can take advantage of the exposed json() method of the Response Promise to wait on the response and the body extraction method at the same time.

  const body = await ky.post('http://localhost:8080/simple-post', { json: { value: "hello world" } }).json();
  console.log(body);

main.js


Sending form data is identical to fetch, create a FormData object, and assign it to the body option. Unlike fetch, ky automatically set the Content-Type header to multipart/form-data.

  const formData = new FormData();
  formData.append('value1', '10');
  formData.append('value2', 'ten');

  await ky.post('http://localhost:8080/multipart-post', {
    body: formData
  });

main.js

If you want to send a request with the Content-Type: application/x-www-form-urlencoded header create a URLSearchParams object and assign that to the body option.

  const searchParams = new URLSearchParams();
  searchParams.set('value1', '10');
  searchParams.set('value2', 'ten');

  await ky.post('http://localhost:8080/formurlencoded-post', {
    body: searchParams
  });

main.js

Download Progress

ky allows your application to install a download progress event handler that is regularly called when ky fetches a resource and informs your application about the current progress (percent, transferred bytes).

  await ky.get('http://localhost:8080/download', {
    onDownloadProgress: (progress, chunk) => {
      console.log(`${progress.percent * 100}% - ${progress.transferredBytes} of ${progress.totalBytes} bytes`);
    }
  });

main.js

The event handler receives a progress and chunk argument. Before ky starts the download it calls your event handler with progress.percent === 0 and progress.transferredBytes === 0. In this very first call, the chunk parameter is an empty Uint8Array. In subsequent calls chunk holds an Uint8Array with the transferred bytes since the last onDownloadProgress call, not the full download.

The progress object contains the following elements: percent, transferredBytes and totalBytes. If the server does not send a proper Content-Length header, it's not possible for ky to figure out the total length; in that case, totalBytes will be 0.

Error Handling

One significant difference to fetch is that ky throws an exception (HTTPError) if the response status code is not between 200 and 299.

In this demo application, the URL /notfound returns a 404 status code. Fetch treats this as a normal response, does not throw an exception but sets the response.ok property to false. Fetch only throws exceptions when the request fails because of a network issue (server down, domain invalid, ...).

  let response = await fetch('http://localhost:8080/notfound');
  console.log("response.ok = ", response.ok);

main.js

On the other hand, ky throws an HTTPError exception not only for network issues but also for each response where the status code is not between 200 and 299.

  try {
    response = await ky.get('http://localhost:8080/notfound');
  } catch (e) {
    console.log('ky.get', e);
  }

main.js

You can disable this feature by setting the throwHttpErrors option to false. ky then behaves like the Fetch API and only throws exceptions for network issues.

  response = await ky.get('http://localhost:8080/notfound', { throwHttpErrors: false });
  console.log("response.ok = ", response.ok);

main.js

Retry

Retry is a feature of ky. By default, this feature is enabled, and ky resends a failed request a second time. If the second attempt fails too, ky throws an exception.

ky sends by default retry requests in case the server responds with one of these status codes: 408, 413, 429, 500, 502, 503, 504. Also, by default, only for the GET, PUT, HEAD, DELETE, OPTIONS, and TRACE methods. Both the status codes and methods can be changed with methods, statusCodes and afterStatusCodes. The option statusCodes specifies the status codes allowed to retry and the afterStatusCodes option specifies the status codes allowed to retry with Retry-After header.

You can change the number of retry attempts with the retry option.

  response = await ky.get('http://localhost:8080/retry', { retry: 5 }).text();

main.js

You can disable the feature by setting the option to 1.

    const response = await ky.get('http://localhost:8080/retry-test', { retry: 1 }).text();

main.js

If you set other retry options, you specify the number of retry attempts with the limit option.

  response = await ky.get('http://localhost:8080/retry', {
    retry: {
      limit: 10,
      methods: ['get'],
      afterStatusCodes: [429]
    }
  }).text();

main.js

Retry respects the Retry-After response header and waits the specified amount of time until it resends a failed request.

With maxRetryAfter, you control the behavior of ky when processing the Retry-After response header. If maxRetryAfter is set to undefined, it will use options.timeout. If Retry-After header is greater than maxRetryAfter, it will cancel the request.

If the server does not send a Retry-After header, ky calculates the retry delay with the following formula:

delay = 0.3 * (2 ^ (retryCount - 1)) * 1000

// 1. attempt --> wait 0.3 seconds 
// 2. attempt --> wait 0.6 seconds 
// 3. attempt --> wait 1.2 seconds
// 4. attempt --> wait 2.4 seconds 
// ....

Timeout

Timeout is another new feature of ky. This feature, like the previous retry feature, is enabled by default, and every ky call will throw a TimeoutError exception if the server does not respond for 10 seconds. You can change the timeout with the timeout option, which expects the timeout in milliseconds.

  console.log('ky, timeout 1s');
  try {
    console.log('request ', Date.now());
    response = await ky.get('http://localhost:8080/timeout', { timeout: 1000 });
  } catch (e) {
    console.log('response', Date.now());
    console.log(e);
  }

main.js

If you want to disable the timeout and wait as long as possible for a response, assign false to the timeout option. By disabling the timeout, you get the behavior of the native Fetch API.

  response = await ky.get('http://localhost:8080/timeout', { timeout: false });

main.js

Abort

Aborting a request is a feature of the Fetch API. You can use this feature with ky the same way as you use it with fetch. Create an AbortController, pass the controller.signal as option to the request call and abort the request with controller.abort().

  const controller = new AbortController();

  setTimeout(() => {
    console.log('abort', Date.now());
    controller.abort();
  }, 2500);

  try {
    console.log('request', Date.now());
    const body = await ky.get('http://localhost:8080/timeout', { signal: controller.signal }).text();
  } catch (error) {
    console.log('exception', Date.now());
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  }

main.js

This is a feature that was added later to the Fetch API. You might encounter some browsers in the wild that implement the Fetch API but not the AbortController. See the Browser compatibility matrix on MDN: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility

Hooks

With hooks, another new feature of ky, you can inject methods into the request/response lifecycle. ky supports before request and after response hooks.

To configure hooks, you create an object with the beforeRequest and afterResponse property and then assign it to the option hooks. You don't have to specify both hooks. Both properties expect an array of functions. As parameters, the beforeRequest hooks receive the request and options objects, and the afterResponse hooks receive the request instance, options, and response objects. Return values from beforeRequest hooks will be ignored. When an afterResponse hook returns an object of type Response, ky overwrites the original response. Return values of other types will be ignored.

In the example below, we add one additional header to the request before ky sends it to the server, and after the response comes back, we create a new response object and overwrite the response from the server.

  const hooks = {
    beforeRequest: [
      (request, options) => {
        console.log('before request');
        options.headers.set('x-api-key', '1111');
      }
    ],
    afterResponse: [
      (request, options, response) => {
        console.log('after response');
        console.log(response);
        // return different response
        return new Response('{"value": "something else"}', { status: 200 });
      }
    ]
  };


  const body = await ky.get('http://localhost:8080/simple-get', { hooks }).json();

main.js

Custom default instances

Another new option we haven't discussed so far is prefixUrl. With this option, you can specify a base URL that will be prepended to the input URL.

Without prefixUrl

const body = await ky.get('http://localhost:8080/simple-get').text();

With prefixUrl

  let body = await ky.get('simple-get', { prefixUrl: 'http://localhost:8080' }).text();

main.js

The trailing slash is optional; ky adds it automatically if needed.


By itself, this feature does not look very useful, but in combination with another new ky feature, this becomes very handy. ky allows you to create new ky instances that override default options. For this purpose ky provides the ky.extend(defaultOptions) and ky.create(defaultOptions) methods.

Somewhere at the start of your application, you create a custom ky instance

  const customKy = ky.create({ prefixUrl: 'http://localhost:8080' });

main.js

In the rest of your application, you use this instance to send requests.

  body = await customKy('simple-get').text();

main.js


extend() inherits the defaults from its parent, while create() creates an instance with completely new default values.

In the example below, we extend the custom ky instance from above and add a custom request header. This results in a ky instance where both options, prefixUrl and headers, have a default value assigned.

  const customApiKy = customKy.extend({ headers: { 'x-api-key': '1111' } });
  body = await customApiKy('simple-get').text();

main.js


This concludes my tutorial about ky. An elegant HTTP client built on top of the Fetch API, that adds new features and simplifies everyday tasks.

Check out the project page for more information: https://github.com/sindresorhus/ky

You find the source code for all the examples in this blog post on GitHub:
https://github.com/ralscha/blog2019/tree/master/ky