- Make sure you have
node
andyarn
installed. - Run
yarn install
- Run
yarn dev
- open
http://localhost:3000/
Fetch (link)
let promise = fetch(url, options);
Without options, it's a simple GET request
-
First, the promise, returned by fetch, resolves with an object of the built-in Response class as soon as the server responds with headers.
-
We can check the http status using
status
(status codes) orok
boolean property.
const response = await fetch(url);
if (response.ok) {
const json = await response.json();
} else {
console.error(`http error: ${response.status}`);
}
- Response provides multiple promise-based methods to access the body in various formats:
a. response.text() - read the response and return as text b. response.json() - read the response and return as json c. response.formData() - read the response and return FormData object d. response.blob() - return response as BLOB (binary data with type) e. response.arrayBuffer() - return response as ArrayBuffer (low-level representation of binary data) f. response.body - it is a ReadableStream object, it allows you read the data as a continuous stream.
We can choose only one body-reading method. If we’ve already got the response with response.text(), then response.json() won’t work, as the body content has already been processed.
const text = await response.text(); // return response
const json = await response.json(); // fails (already consumed)
To set request header in fetch, we can use headers
option.
const response = await fetch(url, {
headers: {
Authentication: 'secret'
}
});
To make a post request, fetch should have following options:
- method:
POST
- body: a. string (eg., JSON encoded) b. FormData object c. Blob
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
});
const result = await response.json();
FormData objects are used to capture HTML form and submit it using fetch or another network method. We can either create new FormData(form) from an HTML form, or create a object without a form at all, and then append fields with methods:
- formData.append(name, value)
- formData.append(name, blob, filename)
- formData.set(name, value)
- formData.set(name, blob, filename)
a. The set method removes fields with the same name, append doesn’t. That’s the only difference between them. b. To send a file, 3-argument syntax is needed, the last argument is a file name, that normally is taken from user filesystem for <input type="file">.
- formData.delete(name)
- formData.get(name)
- formData.has(name)
We can use response.body to a get a readable stream. For eg.,
const reader = await response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(`Received ${value.length} bytes`);
}
- done -
true
when reading is complete, otherwisefalse
. - value - a typed array of bytes:
Uint8Array
const downloadProgress = async () => {
const response = await fetch(
'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100'
);
const totalResponseLength = response.headers.get('Content-Length') ?? 0;
const reader = await response.body.getReader();
const chunks = [];
let receivedResponseLength = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedResponseLength += value.length;
console.log(`received ${receivedResponseLength} of ${totalResponseLength}`);
}
const chunksAll = new Uint8Array(receivedResponseLength);
let position = 0;
for (let chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
const result = new TextDecoder('utf-8').decode(chunksAll);
return JSON.parse(result);
};
downloadProgress().then(console.log).catch(console.error);
There’s a special built-in object for aborting an asynchronous task: AbortController. It can be used to abort not only fetch, but other asynchronous tasks as well.
const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener('abort', () => console.log('aborted'));
// aborts
controller.abort();
console.log(signal.aborted);
Using with fetch:
const abortControllerWithFetch = async () => {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 100);
const response = await fetch(
'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100',
{
signal: controller.signal
}
);
return response.json();
};
abortControllerWithFetch()
.then(console.log)
.catch((err) => console.error(err.name));
AbortController is scalable, it allows to cancel multiple fetches at once
CORS stands for cross origin resource sharing. Cross origin requests - those sent to another domain (even a subdomain) or protocol or port – require special headers from the remote side.
We can perform cross origin request using a script tag. A script could have any src, with any domain, like <script src="http://another.com/…">. It’s possible to execute a script from any website. If a website, e.g. another.com intended to expose data for this kind of access, then a so-called “JSONP (JSON with padding)” protocol was used.
function gotWeather({ temperature, humidity }) {
console.log(`temperature: ${temperature}, humidity: ${humidity}`);
}
const script = document.createElement('script');
script.src = 'http://another.com/weather.json?callback=gotWeather';
document.body.appendChild(script);
There are two types of cross origin requests:
- simple request
- all other request
A request is simple if it fulfills following conditions:
- simple methods are used.
GET
,POST
,HEAD
. - simple headers are used.
a. Accept
b. Accept-Language
c. Content-Language
d. Content-Type =
application/x-www-form-urlencoded
,multipart/form-data
ortext/plain
Any other request is non-simple.
When we try to make a non-simple request, the browser sends another "preflight" request that asks the server - does it agree to accept such cross origin request, or not? And unless server explicitly confirms that with headers, a non simple request is not sent
When we perform a simple cross origin request, browser attaches Origin
in the header. If server responds with
Access-Controll-Allow-Origin=Origin or *
, then the response is successfull, otherwise an error.
For cross origin simple requests, by default following response headers can be accessed:
- Cache-Control
- Content-Length
- Content-Type
- Expires
- Last-Modified
- Pragma
To grant JavaScript access to any other response header, the server must send Access-Control-Expose-Headers header. It contains a comma-separated list of non-simple header names that should be made accessible.
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://my-origin.com
Access-Control-Expose-Headers: Content-Length,API-Key
In case on non simple request, an additional "preflight" request is sent with OPTIONS
method and two headers.
- Access-Control-Request-Method
- Access-Control-Request-Headers
If the server agrees to serve the requests, then it should respond with empty body, status 200 and headers.
- Access-Control-Allow-Origin, either = * or current origin
- Access-Control-Allow-Method
- Access-Control-Allow-Headers
const urlObject = new URL(url, [base]);
- url – the full URL or only path (if base is set, see below),
- base – an optional base URL: if set and url argument has only path, then the URL is generated relative to base.
It gives following options, for url = new URL('https://api.github.com/sarangkartikey50/repos/drawing-board/commits?per_page=100#test')
:
- url.href: = url.toString() will give full url
- url.protocol = https
- url.origin = https://api.github.com:8080 (if port is present)
- url.hostname = api.github.com
- url.port = 8080 (if port is present)
- url.pathname = /sarangkartikey50/respos/drawing-board/commits
- url.search = ?per_page=100
- url.hash = #test
const url = new URL('https://google.com/search?query=JavaScript');
we can use url.searchParams.<methods>
to update search params.
- append(name, value) - url.append('page', '10')
- delete(name) - url.delete('page')
- get(name) - url.get('page') // returns 10
- getAll(name) - url.getAll('page') // returns array values [10]
- has(name) - url.has('page') // returns true or false
- set(name, value) - url.set('page', 11) // sets/repaces a value
- sort() - url.sort() // sorts parameters by name
const url = new URL('https://google.com/search?query=JavaScript rocks');
We can encode & decode strings using following methods:
- encodeURI(url) - encodes url as a whole.
const encodedUrl = encodeURI(url); //https://google.com/search?query=JavaScript%20rocks
- decodeURI(url) - decodes url as a whole.
const decodedUrl = decodeURI(encodeURI); //https://google.com/search?query=Javascript rocks
- encodeURIComponent(component) - encodes a part of url. For eg., searchParams or hash
const encodedSearchParams = encodeURIComponent('Rock&Roll'); //Rock%26Roll
// if we use encodeURL, then result will Rock&Roll only
- decodeURIComponent(component) - decodes a part of url.
const decodedSearchParams = decodeURIComponent(encodedSearchParams); //Rock&Roll
Fetch can't track upload progress
.
const xhr = new XMLHttpRequest();
xhr.open(
'GET',
'https://api.github.com/repos/sarangkartikey50/drawing-board/commits'
);
xhr.send();
// will be called after response is fetched
xhr.onload = function () {
if (xhr.status !== 200) {
console.error(`There was some error: ${xhr.statusText}`);
} else {
console.log(`Done, got ${xhr.response}`);
}
};
// will update response progress
xhr.onprogress = function (event) {
if (event.lengthComputable) {
console.log(`Received: ${event.loaded}, Total: ${event.total}`);
} else {
console.log(`Received: ${event.total}`);
}
};
// will be called on error
xhr.onerror = function () {
console.error('request failed');
};
It provides following methods:
- xhr.upload.onloadstart - called when upload has started.
- xhr.upload.onprogress - called while upload is in progress
- xhr.upload.onabort - called when upload is aborted.
- xhr.upload.onload - called when upload completed successfully.
- xhr.upload.onerror - called when upload throws error.
- xhr.upload.timeout - sets timeout property.
- xhr.upload.onloadend - called when upload is either completed successfully or throws error.
Resumable file upload (link)
TODO
Long polling (link)
Long polling is the simplest way of persistent connection with server without using any specific protocols like Web Sockets or Server Side Events.
Here, we send request periodically, eg., after every 10 seconds.
Cons:
- Messages are passed with delay of 10 seconds.
- Even if there are no messages, server still gets request from clients.
Flow:
- A request is sent to the server.
- The server doesn't close a connection until it has a message to send.
- When a message appears, the server responds to the request with it.
- Once the browser recieves the message, it immediately sends another request.
If the connection is close or lost, browser immediately sends another request
Client:
const subscribe = async () => {
const response = await fetch('/subscribe');
if (response.status === 502) {
//connection timeout
await subscribe();
} else if (response.status !== 200) {
console.error(`Error: ${response.statusText}`);
await new Promise((resolve) => setTimeout(resolve, 1000)); // resumes after 1 second
await subscribe();
} else {
// success
const message = await response.text();
console.log(`Message: ${message}`);
await subscribe();
}
};
subscribe().catch(console.error);
Server:
const subscribers = new Map();
const onSubscribe = (req, res) => {
let id = Math.random();
while(subscribers.has(id)) {
id = Math.random();
}
subscribers.set(id, res);
req.on('close', () => subscribers.delete(id));
}
const onPublish = (req, res) => {
const message = res.body();
subscribers.forEach(subscribedRes => {
subscribedRes.end(message);
});
subscribers.clear();
}
const app = {}; // express app
app.get('/subscribe', onSubscribe);
app.post('/publish', onPublish);
`The server architecture must be able to work with many pending requests. If run a process per connection, then will consume a lot of memory.
Long polling works in situations where messages are rare. Otherwise we can use WebSockets or Server Side Events.
WebSocket (link)
The WebSocket protocol provides a persistent connection between client and server. The data can be passed as packets
without breaking the connection along with some http headers.
WebSocket is really great for systems which requires persistent connection like online games, trading systems, chat apps.
To open a web socket connection, we need to create a "new WebSocket" instance with a special "ws:" protocol in the url.
const socket = new WebSocket('ws://localhost:3000');
we can also use wss for encryption like https.
Events available:
- open - connection established.
- message - data received.
- close - connection gets closed.
- error - websocket error.
We can send the data using socket send socket.send(data)
.
When new WebSocket(url) is called, the browser starts connecting immediately.
- During the connection, the browser sends a GET request to the server confirm websocket support using headers.
- If server sends ok, then websocket connecton is established.
- check process here
For eg.,
const socket = new WebSocket('ws://localhost:3000/chat');
Following headers are sent:
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
- Connection: Upgrade - Signals that the client would like to change protocol.
- Upgrade - Signals that the client would like to use websocket protocol.
- Sec-WebSocket-Key: random key generated by the browser for security purposes.
- Sec-WebSocket-Version: Websocket version supported by client.
If the server agrees to the upgrade protocol, it should send 101 response.
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
WebSocket communication consists of frames
- data fragments, which can be sent from either side.
- Text Frame - contains text
- Binary Frame - contains binary data
- Ping/Pong Frame - Used to check connection, sent from server. Browsers automatically responds to these, etc.
socket.send() allows data to be sent as string or binary inluding Blob, ArrayBuffers
.
When we receive data, it always comes string. We can choose binary using socket.binaryType. We can either set it to 'blob' or 'arraybuffer'. Default is 'blob'.
socket.binaryType = 'arraybuffer';
socket.onmessage = function(event) {
// data will come either as string or arraybuffer
}
It is possible that the user might be on a slow internet connection. In case data to be sent is buffered.
We can check every 100ms whether is buffered amount is 0 or not and then send the data using socket.send()
.
setInterval(function() {
if(socket.bufferedAmount === 0) socket.send(data);
}, 100);
When a party (client or server) wants to close a connection, they send connection close frame using socket.close([code], [reason])
// server
socket.close(1000, 'Done');
// client
socket.onclose = function(event) {
// event.code = 1000
// event.reason = 'Done'
// event.wasClean = true (clean close)
}
We can check the websocket connection state using socket.readyState
.
- 0 - 'Connecting' the connection hasn't been established yet.
- 1 - 'OPEN' communicating.
- 2 - 'CLOSING' connection is closing.
- 3 - 'CLOSED' connection is closed.
Server sent events uses built in class EventSource, that keeps connection with the server and allow to receive events from it. It is similar to WebSocket protocol.
- WebSocket is bidirectional, Server sent events can be sent from server only.
- WebSocket uses websocket protocol, Server sent events use http.
- WebSocket can exchange binary or text data, Server sent events can exchange only text data.
EventSource is a less powerful way of communicating with the server than websocket.
EventSource supports auto reconnection.
const eventSource = new EventSource('http://localhost:3000/events');
eventSource.onmessage = function(event) {
console.log(event.data);
}
Reconnection happens automatically in case on server sent events after couple of seconds.
Although server may send retry: 1500
alongwith data or standalone.
- If the server wants to stop the browser from reconnecting, it should send response with status code 204.
- If the browser wants to close the connection, it should call
eventSource.close()
.
If the connection is closed, there's no way to reconnecting it. We need to create new EventSource object.
When the connection is broken, either side don't know which messages were received. We should always send id along with data.
When id is sent, it is set as eventSource.lastEventId.
Upon reconnection, a header is sent Last-Event-ID with that id so that the server can resend the lost messages again.
id is appended below data by the server so that it can set into eventSource.lastEventId.
By default event source generates three events:
- message - a message is received as event.data
- open - the connection is open.
- error - the connection could not be established.
const eventSource = new EventSource('http:localhost:3000/events');
eventSource.addEventListener('join', event => {
console.log(`Joined: ${event.data}`);
});
eventSource.addEventListener('message', event => {
console.log(`Message: ${event.data}`);
});
eventSource.addEventListener('leave', event => {
console.log(`Left: ${event.data}`);
});