fetch() is an upgraded version of XMLHttpRequest used to make HTTP requests inside JavaScript scripts.

The browser provides this object natively. This article describes its usage in detail.

fetch

1. Basic usage

The function of fetch() is basically the same as XMLHttpRequest, but with three main differences.

  1. fetch() uses Promise and does not use a callback function, so it is much simpler to write and more concise.
  2. The modular design of fetch(), with API spread over multiple objects (Response object, Request object, Headers object), is more reasonable; in contrast, the API design of XMLHttpRequest is not very good, with input, output and state all managed in the same interface, making it easy to write very confusing code.
  3. fetch() processes data through a data stream (Stream object), which can be read in chunks, which is good for improving website performance and reducing memory usage, and is quite useful for requesting large files or slow internet scenarios. The XMLHTTPRequest object does not support data streams, all the data must be placed in the cache, does not support reading in chunks, you must wait until you get it all and then spit it out at once.

In terms of usage, fetch() accepts a URL string as an argument, makes a GET request to that URL by default, and returns a Promise object. Its basic usage is as follows.

1
2
3
fetch(url)
  .then(...)
  .catch(...)

Here is an example to get JSON data from the server.

1
2
3
4
fetch('https://api.github.com/users/ruanyf')
  .then(response => response.json())
  .then(json => console.log(json))
  .catch(err => console.log('Request Failed', err)); 

In the above example, the response received by fetch() is a Stream object, and response.json() is an asynchronous operation that takes out all the content and converts it to a JSON object.

Promise can be rewritten using the await syntax to make the semantics clearer.

1
2
3
4
5
6
7
8
9
async function getJSON() {
  let url = 'https://api.github.com/users/ruanyf';
  try {
    let response = await fetch(url);
    return await response.json();
  } catch (error) {
    console.log('Request Failed', error);
  }
}

In the example above, the await statement must be placed inside the try.... .catch inside so as to catch any errors that may occur in the asynchronous operation.

The latter are written in the await style, not in the .then() style.

2. the Response object: handling HTTP responses

2.1 Synchronization Properties of Response Object

When a fetch() request succeeds, a Response object is obtained. It corresponds to the HTTP response from the server.

1
const response = await fetch(url);

As mentioned before, Response contains data that is read asynchronously through the Stream interface, but it also contains some synchronous properties that correspond to the header information (Headers) of the HTTP response, which can be read immediately.

1
2
3
4
5
async function fetchText() {
  let response = await fetch('/readme.txt');
  console.log(response.status); 
  console.log(response.statusText);
}

In the above example, response.status and response.statusText are the synchronization properties of Response, which can be read immediately.

The header information attributes are as follows.

Response.ok

The Response.ok property returns a boolean value indicating whether the request was successful or not, true corresponding to the status codes 200 to 299 for HTTP requests and false corresponding to the other status codes.

Response.status

The Response.status property returns a number indicating the status code of the HTTP response (e.g. 200, for a successful request).

Response.statusText

The Response.statusText property returns a string indicating the status of the HTTP response (for example, the server returns “OK” after a successful request).

Response.url

The Response.url property returns the URL of the request. If there is a jump in the URL, this property returns the final URL. (return “OK”).

Response.type

The Response.type property returns the type of the request. The possible values are as follows.

  • basic: normal request, i.e. homologous request.
  • cors: cross-domain request.
  • error: network error, mainly used for Service Worker.
  • opaque: This value is returned if the type attribute of the fetch() request is set to no-cors, see the request section for details. Indicates that a simple cross-domain request is being made, similar to the kind of cross-domain request made by <form> forms.
  • opaqueredirect: This value is returned if the redirect attribute of the fetch() request is set to manual, see the request section for details.

Response.redirected

The Response.redirected property returns a boolean value indicating whether the request has been redirected.

2.2 Determining whether a request is successful or not

After fetch() issues a request, there is an important note: fetch() will only report an error if there is a network error, or if the connection cannot be made, but in all other cases it will not report an error and will consider the request successful.

This means that fetch() will not report an error even if the status code returned by the server is 4xx or 5xx (i.e. Promise will not change to the rejected state).

The only way to tell if a request is successful is to get the real status code of the HTTP response through the Response.status property. See the following example.

1
2
3
4
5
6
7
8
async function fetchText() {
  let response = await fetch('/readme.txt');
  if (response.status >= 200 && response.status < 300) {
    return await response.text();
  } else {
    throw new Error(response.statusText);
  }
}

In the above example, the response.status property is only equal to 2xx (200~299) to determine the success of the request. There is no need to consider URL bounces (with status code 3xx), because fetch() will automatically turn the bounced status code to 200.

Another way is to determine if response.ok is true.

1
2
3
4
5
if (response.ok) {
  // request success
} else {
  // request fail
}

2.3 Response.headers property

The Response object also has a Response.headers property, which points to a Headers object corresponding to all headers of the HTTP response.

The Headers object can be traversed using a for. . of loop to iterate through them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const response = await fetch(url);

for (let [key, value] of response.headers) { 
  console.log(`${key} : ${value}`);  
}

// or
for (let [key, value] of response.headers.entries()) { 
  console.log(`${key} : ${value}`);  
}

The Headers object provides the following methods to manipulate headers.

  • Headers.get(): Returns the key value, based on the specified key name.
  • Headers.has(): returns a boolean value indicating whether or not a header is contained.
  • Headers.set(): sets the specified key name to the new key value, or adds it if it does not exist.
  • Headers.append(): add the header.
  • Headers.delete(): delete the header.
  • Headers.keys(): return a traverser that iterates through all the key names in order.
  • Headers.values(): returns a traverser that iterates through all the key values in order.
  • Headers.entries(): returns a traverser that iterates through all key-value pairs ([key, value]) in order.
  • Headers.forEach(): iterates through the headers in turn, executing the argument function once for each header.

Some of the methods above can modify headers because they inherit from the Headers interface. For HTTP responses, it makes little sense to modify headers, and many headers are read-only, so browsers do not allow modifications.

The most common of these methods is response.headers.get(), which is used to read the value of a header.

1
2
3
4

let response =  await  fetch(url);  
response.headers.get('Content-Type')
// application/json; charset=utf-8

The Headers.keys() and Headers.values() methods are used to iterate over the key names and key values of the headers, respectively.

1
2
3
4
5
6
7
8
9
// key
for(let key of myHeaders.keys()) {
  console.log(key);
}

// value
for(let value of myHeaders.values()) {
  console.log(value);
}

The Headers.forEach() method can also iterate through all the keys and key names.

1
2
3
4
let response = await fetch(url);
response.headers.forEach(
  (value, key) => console.log(key, ':', value)
);

2.4 Methods for reading content

The Response object provides different read methods depending on the different types of data returned by the server.

  • response.text(): get the text string.
  • response.json(): get JSON object.
  • response.blob(): get the binary Blob object.
  • response.formData(): get the FormData form object.
  • response.arrayBuffer(): get the binary ArrayBuffer object.

The 5 read methods above are all asynchronous and return a Promise object. You must wait until the asynchronous operation is finished to get the complete data returned by the server.

response.text()

response.text() can be used to get text data, such as HTML files.

1
2
3
const response = await fetch('/users.html');
const body = await response.text();
document.body.innerHTML = body

response.json()

response.json() is mainly used to get the JSON data returned by the server, which has been given as an example earlier.

response.formData()

response.formData() is mainly used in the Service Worker to intercept user-submitted forms, modify some data, and then submit them to the server.

response.blob()

response.blob() is used to get the binary file.

1
2
3
4
5
6
const response = await fetch('flower.jpg');
const myBlob = await response.blob();
const objectURL = URL.createObjectURL(myBlob);

const myImage = document.querySelector('img');
myImage.src = objectURL;

The above example reads the image file flower.jpg and displays it on the web page.

response.arrayBuffer()

response.arrayBuffer() is mainly used to get the streaming file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const audioCtx = new window.AudioContext();
const source = audioCtx.createBufferSource();

const response = await fetch('song.ogg');
const buffer = await response.arrayBuffer();

const decodeData = await audioCtx.decodeAudioData(buffer);
source.buffer = buffer;
source.connect(audioCtx.destination);
source.loop = true;

The above example is response.arrayBuffer() to get the audio file song.ogg and then play it online.

2.5 Response.clone()

The Stream object can only be read once, and when it’s done, it’s gone. This means that only one of the five read methods in the previous section can be used, otherwise an error will be reported.

1
2
let text =  await response.text();
let json =  await response.json();  // error

The above example uses response.text() first, and then the Stream is read out. If you call response.json() later, there is no more content to read, so an error is reported.

The Response object provides the Response.clone() method to create a copy of the Response object for multiple reads.

1
2
3
4
5
6
7
8
const response1 = await fetch('flowers.jpg');
const response2 = response1.clone();

const myBlob1 = await response1.blob();
const myBlob2 = await response2.blob();

image1.src = URL.createObjectURL(myBlob1);
image2.src = URL.createObjectURL(myBlob2);

In the above example, response.clone() makes a copy of the Response object, and then reads the same image twice.

The Response object also has a Response.redirect() method that redirects the Response result to the specified URL. This method is generally only used inside the Service Worker, so it is not described here.

2.6 The Response.body property

The Response.body property is the underlying interface exposed by the Response object, returning a ReadableStream object for the user to manipulate.

It can be used to read content in chunks, and one application is to display the progress of a download.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

const response = await fetch('flower.jpg');
const reader = response.body.getReader();

while(true) {
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  console.log(`Received ${value.length} bytes`)
}

In the above example, the response.body.getReader() method returns a traverser. The read() method of this traverser returns one object at a time, representing the block of content read this time.

The done property of this object is a Boolean value that determines whether the read is complete; the value property is an arrayBuffer array that represents the contents of the block, and the value.length property is the size of the current block.

3. the second parameter of fetch(): custom HTTP request

The first argument to fetch() is the URL, and it can also accept a second argument as a configuration object to customize the outgoing HTTP request.

1
fetch(url, optionObj)

The optionObj of the above command is the second parameter.

The method, header, and data body of the HTTP request are set inside this object. Here are some examples.

POST request

1
2
3
4
5
6
7
8
9
const response = await fetch(url, {
  method: 'POST',
  headers: {
    "Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  },
  body: 'foo=bar&lorem=ipsum',
});

const json = await response.json();

In the above example, the configuration object uses three properties.

  • method: method of the HTTP request, POST, DELETE, PUT are set in this property.
  • headers: an object to customize the headers of the HTTP request.
  • body: the data body of the POST request.

Note that some headers cannot be set via the headers attribute, such as Content-Length, Cookie, Host and so on. They are automatically generated by the browser and cannot be modified.

Submit JSON data

1
2
3
4
5
6
7
8
9

const user =  { name:  'John', surname:  'Smith'  };
const response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
   'Content-Type': 'application/json;charset=utf-8'
  }, 
  body: JSON.stringify(user) 
});

In the above example, the header Content-Type should be set to ‘application/json;charset=utf-8’. Because the default sent is plain text, the default value of Content-Type is ’text/plain;charset=UTF-8’.

Submission Form

1
2
3
4
5
6
const form = document.querySelector('form');

const response = await fetch('/users', {
  method: 'POST',
  body: new FormData(form)
})

File upload

If there is a file selector inside the form, you can use the previous example to write the uploaded file to be included in the whole form and submit it together.

Another way is to add files with a script to construct a form for uploading, see the example below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const input = document.querySelector('input[type="file"]');

const data = new FormData();
data.append('file', input.files[0]);
data.append('user', 'foo');

fetch('/avatars', {
  method: 'POST',
  body: data
});

When uploading binary files, you don’t need to change the Content-Type in the header, the browser will set it automatically.

Upload binary data directly

fetch() can also upload binary data directly, putting Blob or arrayBuffer data inside the body property.

1
2
3
4
5
6
7
8
let blob = await new Promise(resolve =>   
  canvasElem.toBlob(resolve,  'image/png')
);

let response = await fetch('/article/fetch/post/image', {
  method:  'POST',
  body: blob
});

4. The full API for the fetch() configuration object

The full API for the second parameter of fetch() is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const response = fetch(url, {
  method: "GET",
  headers: {
    "Content-Type": "text/plain;charset=UTF-8"
  },
  body: undefined,
  referrer: "about:client",
  referrerPolicy: "no-referrer-when-downgrade",
  mode: "cors", 
  credentials: "same-origin",
  cache: "default",
  redirect: "follow",
  integrity: "",
  keepalive: false,
  signal: undefined
});

The underlying fetch() request uses the interface of the Request() object with exactly the same parameters, so the API above is also the API of Request().

Among these properties, headers, body, and method have been given as examples earlier, and the following is an introduction to the other properties.

cache

The cache property specifies how the cache is handled. The possible values are as follows.

  • default: default value, look for matching requests inside the cache first.
  • no-store: request the remote server directly, and do not update the cache.
  • reload: request the remote server directly and update the cache.
  • no-cache: compare server resources with local cache, use server resources only if there is a new version, otherwise use cache.
  • force-cache: cache takes precedence and requests the remote server only if no cache exists.
  • only-if-cached: only check the cache, if it does not exist inside the cache, a 504 error will be returned.

mode

The mode attribute specifies the mode of the request. The possible values are as follows.

  • cors: default value, cross-domain requests are allowed.
  • same-origin: only same-origin requests are allowed.
  • no-cors: request methods are limited to GET, POST and HEAD, and only a limited number of simple headers can be used, no cross-domain complex headers can be added, equivalent to the requests that can be made by submitting a form.

credentials

The credentials attribute specifies whether to send cookies. The possible values are as follows.

  • same-origin: default value, send Cookie for same-origin requests, not for cross-domain requests.
  • include: Cookie is sent for both same-origin and cross-domain requests.
  • omit: no cookie is sent for all requests.

Cross-domain requests send Cookie and require the credentials property to be set to include.

1
2
3
4

fetch('http://another.com', {
  credentials: "include"
});

signal

The signal attribute specifies an AbortSignal instance to cancel the fetch() request, as detailed in the next section.

keepalive

The keepalive property is used when the page is offloaded to tell the browser to stay connected in the background and continue sending data.

A typical scenario is when a user leaves a web page and the script submits some statistics about the user’s behavior to the server. At this point, if the keepalive property is not used, the data may not be sent because the browser has unloaded the page.

1
2
3
4
5
6
7
window.onunload = function() {
  fetch('/analytics', {
    method: 'POST',
    body: "statistics",
    keepalive: true
  });
};

redirect

The redirect attribute specifies the method of handling HTTP jumps. The possible values are as follows.

  • follow: default value, fetch() follows HTTP jumps.
  • error: fetch() reports an error if a jump occurs.
  • manual: fetch() does not follow HTTP jumps, but the response.url property will point to the new URL and the response.redirected property will change to true, leaving it up to the developer to decide how to handle subsequent jumps.

integrity

The integrity attribute specifies a hash value to check that the data returned by the HTTP response is equal to this pre-defined hash value.

For example, when downloading a file, check that the SHA-256 hash of the file matches to ensure that it has not been tampered with.

1
2
3
4

fetch('http://site.com/file', {
  integrity: 'sha256-abcdef'
});

referrer

The referrer attribute is used to set the referer header for fetch() requests.

This attribute can be any string, or it can be set to an empty string (i.e. no referer header is sent).

1
2
3
4

fetch('/page', {
  referrer: ''
});

referrerPolicy

The referrerPolicy property is used to set the rules for the Refererer header. The possible values are as follows.

  • no-referrer-when-downgrade: default, always sends the Referer header, unless it is not sent when requesting HTTP resources from HTTPS pages.
  • no-referrer: do not send the Referer header. origin: The Referer header contains only the domain name, not the full path.
  • origin-when-cross-origin: same origin request Referer header contains full path, cross-domain request contains only domain name.
  • same-origin: cross-domain request does not send the Referer, same-origin request sends it.
  • strict-origin: The Referer header contains only the domain name, and the Referer header is not sent for HTTPS pages requesting HTTP resources.
  • strict-origin-when-cross-origin: The Referer header contains the full path for same-origin requests, and only the domain name for cross-domain requests; it is not sent when HTTPS pages request HTTP resources.
  • unsafe-url: always send the Referer header regardless of the case.

5. Cancel fetch() request

After the fetch() request is sent, if you want to cancel it in the middle, you need to use the AbortController object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let controller = new AbortController();
let signal = controller.signal;

fetch(url, {
  signal: controller.signal
});

signal.addEventListener('abort',
  () => console.log('abort!')
);

controller.abort(); // cancel

console.log(signal.aborted); // true

In the above example, first a new AbortController instance is created, then a fetch() request is sent, and the signal property of the configuration object must specify to receive the signal controller.signal from the AbortController instance.

The controller.abort() method is used to send the abort signal. This triggers the abort event, which can be listened to, or the controller.signal.aborted property can be used to determine if the cancel signal has been sent.

Here is an example of an automatic request cancellation after 1 second.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
  let response = await fetch('/long-operation', {
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') {
    console.log('Aborted!');
  } else {
    throw err;
  }
}

Reference https://www.ruanyifeng.com/blog/2020/12/fetch-tutorial.html