Networking
The fetch()
function is a promise-based API for making HTTP and HTTPS requests.
fetch("https://jsonplaceholder.typicode.com/users/1")
.then((response) => response.json())
.then((user) => console.log(user.name));
// Output: Leanne Graham
Using async and await keywords
You can also use fetch()
function with async
and await
keywords to achieve the same results.
async function getUser() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/2");
const user = await response.json();
console.log(user.name);
}
getUser();
// Output: Ervin Howell
Response status and headers
The fetch()
resolves the promise when the server's response status and headers become available. It is a good idea to check response headers and include a catch()
clause every time you make a fetch()
call.
fetch("https://jsonplaceholder.typicode.com/users/3")
.then((response) => {
if (!response.ok) {
throw new Error(`Unexpected response status ${response.status}`);
}
if (!response.headers.get("content-type").includes("application/json")) {
throw new Error(
`Unexpected content type ${response.headers.get("content-type")}`
);
}
return response.json();
})
.then((user) => console.log(user.name))
.catch((err) => console.error("Error catching the user", err));
// Output: Clementine Bauch
Passing a URL object
The fetch()
function accepts URL objects as its first argument.
const url = new URL("https://jsonplaceholder.typicode.com/users/4");
fetch(url)
.then((response) => response.json())
.then((user) => console.log(user.name));
// Output: Patricia Lebsack
Parsing response bodies
In addition to json()
and text()
methods, the Response object also has the following methods to obtain the body of the response:
arrayBuffer()
method returns a promise that resolves to an ArrayBuffer object that contains binary data. You can use ArrayBuffer to create a typed array or a DataView object.blob()
method return a promise that resolves to a Blob object that contains large binary data.formData()
method returns a promise that resolves to a FormData object that contains data encoded in "multipart/form-data" format.
Setting request headers
The Headers interface allows you to create and manipulate HTTP request and response headers.
const headers = new Headers();
headers.append("Authorization", `Basic ${btoa("USERNAME:PASSWORD")}`);
fetch("https://echo.atsana.com", { headers })
.then((response) => response.json())
.then((data) => console.log(data.headers.authorization));
// Output: Basic VVNFUk5BTUU6UEFTU1dPUkQ=
Setting the request method
By default, fetch()
makes GET requests. However, you can specify a different method (such as POST, PUT, or DELETE). POST and PUT methods typically have a request body containing data to be sent to the server.
fetch("https://echo.atsana.com", {
method: "PUT",
body: JSON.stringify({
id: 1,
title: "foo",
body: "bar",
userId: 1,
}),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
})
.then((response) => response.json())
.then((json) => console.log(json));
With POST requests, you can specify parameter names and values with URLSearchParams object, and then pass the URLSearchParams object as the value of the body
property in a fetch()
request. This way, the parameters are encoded as a query string and sent in the body of the POST request with Content-Type
header automatically set to "application/x-www-form-urlencoded;charset=UTF-8".
const params = new URLSearchParams();
params.append("name", "John Doe");
params.append("email", "[email protected]");
params.append("message", "Hello world");
fetch("https://echo.atsana.com", {
method: "POST",
body: params,
})
.then((response) => response.json())
.then((data) => console.log(data));
FormData objects can be created and initialized with values by passing a < form>
element to the FormData() constructor. But you can also create "multipart/form-data" request bodies by invoking the Formata()
constructor with no arguments and initializing the name/value pairs with the set()
and append()
methods. Then you pass the FormData object as the value of the body
property in a fetch()
request. In this scenario, the Content-Type
header is automatically set to "multipart/form-data; boundary=..." with a unique boundary string that matches the body. Using FormData is particularly useful when you need to upload files or other large data. The FormData object handles the multipart encoding and boundary string automatically.
const formData = new FormData();
formData.append("name", "Joe Black");
formData.append("email", "[email protected]");
formData.append("message", "Hello, this is me.");
fetch("https://echo.atsana.com", {
method: "POST",
body: formData,
})
.then((response) => response.json())
.then((data) => console.log(data));
Streaming response body
You can stream chunks of response body as they arrive over the network to process each one sequentially and report the progress of the download to the user. The body
property of a Response object is a ReadableStream object. If you have already called a response method like text()
or json()
that reads, parses, and returns the body, then bodyUsed
property will be true
to indicate that the body stream has already been read. If bodyUsed
is false
, however, then the stream has not yet been read, in which case you can call getReader()
on body
to obtain a stream reader object, then use the read()
method of this reader object to asynchronously read chunks of text from the stream. The read()
method returns a Promise that resolves to object with done
and value
properties. The done
property will be true
if the entire body has been read or if the stream was closed. The value
property will be next chunk as Uint8Array or undefined
if there are no more chunks.
async function streamBody(response, reportProgress, processChunk) {
let expectedBytes = parseInt(response.headers.get("Content-Length"));
let bytesRead = 0;
let reader = response.body.getReader();
let decoder = new TextDecoder("utf-8");
let body = "";
while (true) {
let { done, value } = await reader.read();
if (value) {
if (processChunk) {
let processed = processChunk(value);
if (processed) {
body += processed;
}
} else {
body += decoder.decode(value, { stream: true });
}
if (reportProgress) {
bytesRead += value.length;
reportProgress(bytesRead, bytesRead / expectedBytes);
}
}
if (done) {
break;
}
}
return body;
}
function update(receivedBytes, progress) {
console.log(
`Recived bytes: ${receivedBytes}, Complete: ${parseInt(100 * progress)}%`
);
}
// Data source: https://data.gov
fetch("https://data.montgomerycountymd.gov/api/views/iv8c-428b/rows.json")
.then((response) => streamBody(response, update))
.then((bodyText) => JSON.parse(bodyText))
.then((json) => console.log(json));
File uploads
Uploading a file from a user's computer to a web server is generally performed by using a FormData object as the request body. A file is obtained from <input type="file">
element on the web page by listening for "change" event on it. When the event occurs, the files
array of the input element should contain at least one File object.
<form id="uploadForm">
<input type="file" name="file" />
<button type="submit">Upload</button>
</form>
const form = document.querySelector("#uploadForm");
form.addEventListener("submit", (e) => {
e.preventDefault();
const file = form.file.files[0];
const formData = new FormData();
formData.set("file", file);
fetch("https://upload.atsana.workers.dev", {
method: "POST",
body: formData,
})
.then((response) => response.json())
.then((data) => {
console.log("Success:", data);
})
.catch((error) => {
console.error("Error:", error);
});
});
You can also implement HTML drag-and-drop API to upload files to the server. You get files from the dataTransfer.files
array of the event object passed to an event listener from "drop" events.
<div id="drop-area">Drop files here</div>
// Get the drop area element
const dropArea = document.getElementById("drop-area");
// Prevent default behaviors
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight the drop area when a file is dragged over
["dragenter", "dragover"].forEach((eventName) => {
dropArea.addEventListener(
eventName,
() => dropArea.classList.add("hover"),
false
);
});
["dragleave", "drop"].forEach((eventName) => {
dropArea.addEventListener(
eventName,
() => dropArea.classList.remove("hover"),
false
);
});
// Handle the drop event
dropArea.addEventListener("drop", handleDrop, false);
function handleDrop(e) {
let dt = e.dataTransfer;
let files = dt.files;
handleFiles(files);
}
function handleFiles(files) {
[...files].forEach(uploadFile);
}
function uploadFile(file) {
let url = "https://upload.atsana.workers.dev";
const formData = new FormData();
formData.set("file", file);
fetch(url, {
method: "POST",
body: formData,
})
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error));
}
````
## Cross-origin requests
In most cases, `fetch()` is used to request data from their own web server with same-origin requests (protocol://hostname:port) as the document that contains the script that is making the request.
For security reasons, web browsers disallow cross-origin requests except for images and scripts. However, Cross-Origin Resource Sharing (CORS) enables safe cross-origin requests.
When `fetch()` is used with cross-origin URL, the browser adds "Origin" header to the request. If the server responds to the request with appropriate "Access-Control-Allow-Origin" header, the request proceeds; otherwise, the Promise returned by `fetch()` is rejected.
```js
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Authorization,*"
}
Aborting a request
You can create an AbortController object before starting the request. The signal
property of the AbortController object is an AbortSignal object. Pass this signal object as the value of the signal
property of the options object that you pass to fetch()
. After that, you can call the abort()
method of the controller object to abort the request, which will cause any Promise object related to the fetch request to reject with an exception.
const options = { timeout: 3000 };
const dataURL = "https://data.wa.gov/api/views/f6w7-q2d2/rows.json";
fetchWithTimout(dataURL, options)
.then((response) => streamBody(response, update))
.then((bodyText) => JSON.parse(bodyText))
.then((json) => console.log(json))
.catch((err) => {
if (err.name === "AbortError") {
console.log("Download aborted:", options.signal.reason);
} else {
console.error("Download failed:", err);
}
});
Request options
An alternative to passing two arguments to fetch()
is to instead pass the same two arguments to the Request()
constructor and then pass the resulting Request object to fetch()
.
const request = new Request("https://echo.atsana.com", {
method: "POST",
body: "Using the Request() constructor",
});
fetch(request)
.then((response) => response.json())
.then((data) => console.log(data));
The Options object that is passed as a second argument to fetch()
or Request()
, support method
, headers
, body
, and other options as well:
cache
property overrides default caching behavior. The values include "default", "no-store", "reload", "no-cache", and "force-cache".redirect
property controls redirect responses from the server. Allowed values are "follow", "error", and "manual".referrer
property is a string that contains a relative URL from which a resource has been requested.
Server-sent events
EventSource()
constructor is part of the Server-Sent Events (SSE) API, which allows web pages to receive updates from a server over a single HTTP connection without having to repeatedly poll the server for updates. This is particularly useful for real-time applications such as live feeds, notifications, or any application where the server needs to push data to the client.
const eventSource = new EventSource("https://stream.atsana.workers.dev/events");
eventSource.addEventListener("bid", function (event) {
console.log(event.type, event.data);
});
eventSource.onerror = function (event) {
console.error("EventSource failed:", event);
};
setTimeout(() => eventSource.close(), 10000);
WebSockets
A WebSocket API is a communication protocol that provides full-duplex communication channels over a single, long-lived connection. It is designed to allow web applications to establish a persistent connection to a server, enabling real-time, two-way communication between a client (usually a web browser) and a server.
You connect to a service using the WebSocket URL that begins with ws://
for non-secure WebSocket connections and wss://
for secure WebSocket connections. To establish a WebSocket connection, the browser first establishes an HTTP connection and send the server Upgrade: websocket
header requesting that the connection be switched from the HTTP protocol to the WebSocket protocol.
const socket = new WebSocket("wss://websocket.atsana.workers.dev");
When a WebSocket transitions from the CONNECTING to OPEN state, it fires an "open" event, which you can listen by setting the onopen
property of the WebSocket object or by calling addEventListener()
on it. If a protocol or other error occurs for a WebSocket connection, the WebSocket object fires an "error" event. When a WebSocket changes to CLOSED state, it fires a "close" event.
socket.onopen = function (event) {
console.log("Connected to the WebSocket server");
};
socket.onerror = function (error) {
console.error("WebSocket error:", error);
};
socket.onclose = function (event) {
console.log("Disconnected from the WebSocket server");
};
To receive messages from a server, register an event handler for "message" events. The object associated with a "message" event is a MessageEvent instance with a data
property that contains the server's message. If the server sends a binary data, then the data
property will be a Blob object. If you wnt to receive binary data as ArrayBuffer instead of Blobs, set the binaryType
property of the WebSocket object to the string "arraybuffer".
socket.onmessage = function (event) {
console.log(`Message from server: ${event.data}`);
};
To send a message to the server, invoke the send()
method of the WebSocket object, which expects a single argument which can be a string, Blob, ArrayBuffer, typed array, or DataView object. The send()
method buffers the message, and the bufferedAmount
property of the WebSocket object specifies the number of bytes that are buffered but not yet sent.
const messageQueue = [];
function sendMessage(message) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
} else {
messageQueue.push(message);
}
}
socket.addEventListener("open", (event) => {
while (messageQueue.length > 0) {
const message = messageQueue.shift();
socket.send(message);
}
});
sendMessage("Message 1");
sendMessage("Message 2");
setTimeout(() => {
sendMessage("Message 3");
}, 2000);
WebSocket protocol negotiation allows the client and server to agree on one or more subprotocols for their communication. This is useful when you have multiple protocols that can be used over a WebSocket connection and you want to choose the most suitable one. When you call the WebSocket()
constructor, passing an array of strings as a second argument, you are specifying a list of application protocols that you know how to handle and asking the server to pick one. During the connection process, the server will choose one of the protocols, which will be stored in the protocol
property of the WebSocket object.
const protocols = ["protocol1", "protocol2"];
const socketProtocol = new WebSocket(
"wss://websocket.atsana.workers.dev",
protocols
);
socketProtocol.addEventListener("open", (event) => {
console.log(
`Connected to WebSocket server with protocol: ${socketProtocol.protocol}`
);
socketProtocol.send("Hello Server!");
});
socketProtocol.addEventListener("message", (event) => {
console.log("Message from protocol server:", event.data);
});
socketProtocol.addEventListener("close", (event) => {
console.log("Disconnected from protocol WebSocket server");
});
socketProtocol.addEventListener("error", (error) => {
console.error("WebSocket error:", error);
});