Exploring the WebTransport API: A New Era of Web Communication
Add to your RSS feed16 January 202516 min readTable of Contents
Discover the WebTransport API, a revolutionary technology that enables efficient, low-latency communication between web clients and servers. Learn how this innovative protocol enhances real-time data transfer, supports bidirectional streams, and improves overall web application performance. Explore its use cases, benefits, and implementation tips to stay ahead in modern web development.
The WebTransport API
offers a modern alternative to WebSockets, facilitating data transmission between clients and servers using the HTTP/3 Transport. It supports multiple streams, unidirectional streams, and out-of-order delivery. WebTransport enables reliable communication through streams and unreliable transport via UDP-like datagrams.
Definition and Features
The WebTransport API
is an interface for transmitting data between clients and servers using the HTTP/3 protocol.
It supports reliable, ordered delivery of data through one or more uni- or bidirectional streams, as well as unreliable, unordered delivery via datagrams. In the former, it serves as an alternative to WebSockets, and in the latter, it acts as an alternative to RTCDataChannel provided by the WebRTC API.
As of now, neither Node.js, Deno, nor Bun support the WebTransport API
.
In June 2023, WebTransport support was added to Socket.io. However, this support relies on the package @fails-components/webtransport, which appears more like an experimental tool not intended for production use. Despite this, let's take a closer look at this option, as Socket.io is a well-established library known for real-time data exchange.
HTTP/3 Overview
HTTP/3 is built on Google’s QUIC protocol, which itself is based on UDP and aims to address several limitations inherent to the TCP protocol:
- Head-of-Line (HOL) Blocking: Unlike HTTP/2, which supports multiplexing (multiple streams transmitted over a single connection), if one stream fails in HTTP/2, others must wait for it to be restored. QUIC eliminates this issue by allowing streams to be independent.
- Higher Performance: QUIC outperforms TCP in various ways. One key reason is that QUIC natively implements security measures (unlike TCP, which relies on TLS), leading to fewer round trips. Additionally, QUIC streams offer a more efficient transport mechanism compared to TCP's packet-based transmission, particularly beneficial in heavily loaded networks.
- Seamless Network Transition: QUIC uses a unique connection identifier that allows packets to be delivered correctly across different networks. This identifier can persist across network changes, enabling uninterrupted downloads even when switching from Wi-Fi to mobile networks. In contrast, HTTP/2 relies on IP addresses, which can cause disruptions during network transitions.
- Unreliable Delivery: HTTP/3 supports unreliable delivery, which can be more efficient than guaranteed delivery in certain scenarios.
To enable HTTP/3 (QUIC) support in Google Chrome, go to chrome://flags
and enable the Experimental QUIC protocol option.
Working Principles of WebTransport API
Establishing a Connection
To establish an HTTP/3 connection with a server, the URL must be passed to the WebTransport()
constructor. Note that the URL should use the HTTPS scheme, and the port must be specified explicitly. The WebTransport.ready
promise resolves once the connection is successfully established.
The connection can be closed using the WebTransport.closed
promise. Any errors encountered are instances of WebTransportError
, which include additional details on top of the standard DOMException
set.
1 const url = "https://example.com:4999/wt";23 async function initTransport(url) {4 // Initialize the connection5 const transport = new WebTransport(url);67 // Resolving this promise indicates readiness to handle requests8 await transport.ready;910 // ...11 }1213 async function closeTransport(transport) {14 // Handle connection closing15 try {16 await transport.closed;17 console.log(`HTTP/3 connection to ${url} closed gracefully.`);18 } catch (error) {19 console.error(`HTTP/3 connection to ${url} closed due to error: ${error}.`);20 }21 }
This setup ensures you can effectively manage connections and handle errors when working with the WebTransport API over HTTP/3.
Unreliable Data Transmission Using Datagrams
Unreliable transmission means that there is no guarantee of complete data delivery or the order in which it arrives. In some cases, this is acceptable, with the main advantage being faster data transfer speeds.
Unreliable data delivery is handled through the WebTransport.datagrams property, which returns a WebTransportDatagramDuplexStream object containing everything necessary for sending datagrams to the server and receiving them on the client.
The WebTransportDatagramDuplexStream.writable property provides a WritableStream object, allowing data to be sent to the server:
1 const writer = transport.datagrams.writable.getWriter();2 const data1 = new Uint8Array([65, 66, 67]);3 const data2 = new Uint8Array([68, 69, 70]);4 writer.write(data1);5 writer.write(data2);
The WebTransportDatagramDuplexStream.readable property provides a ReadableStream, allowing data received from the server to be read:
1 async function readData() {2 const reader = transport.datagrams.readable.getReader();34 while (true) {5 const { value, done } = await reader.read();67 if (done) {8 break;9 }1011 console.log(value); // value is a Uint8Array12 }13 }
Reliable Data Transmission Using Streams
Reliable transmission ensures complete and ordered data delivery, though it takes longer compared to datagrams. However, reliability is critical in many scenarios, such as chat applications.
When using streams for data transmission, it’s possible to define stream priorities.
Unidirectional Data Transmission
To open a unidirectional stream, use the WebTransport.createUnidirectionalStream() method, which returns a WritableStream
. Data is sent to the server using the writer returned by getWriter
:
1 async function writeData() {2 const stream = await transport.createUnidirectionalStream();3 const writer = stream.writable.getWriter();4 const data1 = new Uint8Array([65, 66, 67]);5 const data2 = new Uint8Array([68, 69, 70]);6 writer.write(data1);7 writer.write(data2);89 try {10 await writer.close();11 console.log("All data has been successfully sent");12 } catch (error) {13 console.error(`An error occurred while sending data: ${error}`);14 }15 }
The WritableStreamDefaultWriter.close()
method is used to close the HTTP/3 connection after sending all data.
To extract data from a unidirectional stream opened on the server, use the WebTransport.incomingUnidirectionalStreams
property, which returns ReadableStream
objects of WebTransportReceiveStream
.
Create a function to read from WebTransportReceiveStream
. These objects inherit from the ReadableStream
class, making it easy to implement:
1 async function readData(receiveStream) {2 const reader = receiveStream.getReader();34 while (true) {5 const { done, value } = await reader.read();67 if (done) {8 break;9 }1011 console.log(value); // value is a Uint8Array12 }13 }
Get a reference to reader using getReader()
and read from incomingUnidirectionalStreams
in chunks (each chunk is a WebTransportReceiveStream
):
1 async function receiveUnidirectional() {2 const uds = transport.incomingUnidirectionalStreams;3 const reader = uds.getReader();45 while (true) {6 const { done, value } = await reader.read();78 if (done) {9 break;10 }1112 await readData(value);13 }14 }
These mechanisms allow effective handling of both reliable and unreliable data transmissions using the WebTransport API.
Bidirectional Data Transmission
To open a bidirectional stream, use the WebTransport.createBidirectionalStream() method, which returns a WebTransportBidirectionalStream. This stream contains readable and writable properties that provide references to instances of WebTransportReceiveStream
and WebTransportSendStream
. These can be used for reading data received from the server and sending data to the server, respectively.
1 async function setUpBidirectional() {2 const stream = await transport.createBidirectionalStream();3 // stream is WebTransportBidirectionalStream4 // stream.readable is WebTransportReceiveStream5 const readable = stream.readable;6 // stream.writable is WebTransportSendStream7 const writable = stream.writable;89 // Additional setup code can follow10 }
Reading from WebTransportReceiveStream
can be implemented as follows:
1 async function readData(readable) {2 const reader = readable.getReader();34 while (true) {5 const { value, done } = await reader.read();67 if (done) {8 break;9 }1011 console.log(value); // value is Uint8Array12 }13 }
Writing to WebTransportSendStream
can be implemented as follows:
1 async function writeData(writable) {2 const writer = writable.getWriter();3 const data1 = new Uint8Array([65, 66, 67]);4 const data2 = new Uint8Array([68, 69, 70]);5 writer.write(data1);6 writer.write(data2);7 }
To extract data from a bidirectional stream opened on the server, use the WebTransport.incomingBidirectionalStreams property, which returns ReadableStream
objects of WebTransportBidirectionalStream. Each stream can be used for reading and writing instances of Uint8Array
. You’ll need a function to handle reading from the bidirectional stream:
1 async function receiveBidirectional() {2 const bds = transport.incomingBidirectionalStreams;34 const reader = bds.getReader();56 while (true) {7 const { done, value } = await reader.read();89 if (done) {10 break;11 }1213 await readData(value.readable);14 await writeData(value.writable);15 }16 }
This approach allows the handling of bidirectional data streams efficiently, making full use of the WebTransport API.
How to Implement WebTransport API using JS?
1 // Create a WebTransport connection2 class TransportClient {3 constructor(url) {4 this.url = url;5 this.transport = null;6 this.streams = new Map();7 this.datagramWriter = null;8 this.datagramReader = null;9 }1011 async connect() {12 try {13 this.transport = new WebTransport(this.url);14 console.log('Initiating connection...');1516 // Wait for connection establishment17 await this.transport.ready;18 console.log('Connection established successfully');1920 // Set up error handling21 this.transport.closed22 .then(() => {23 console.log('Connection closed normally');24 })25 .catch((error) => {26 console.error('Connection closed due to error:', error);27 });2829 // Initialize datagram handlers30 this.setupDatagrams();3132 } catch (error) {33 console.error('Failed to establish connection:', error);34 throw error;35 }36 }3738 // Set up datagram sending and receiving39 setupDatagrams() {40 // Set up datagram writer41 this.datagramWriter = this.transport.datagrams.writable.getWriter();4243 // Set up datagram reader44 this.handleDatagrams();45 }4647 async handleDatagrams() {48 try {49 const reader = this.transport.datagrams.readable.getReader();50 while (true) {51 const {value, done} = await reader.read();52 if (done) {53 console.log('Datagram reader done');54 break;55 }56 // Process received datagram57 const decoded = new TextDecoder().decode(value);58 console.log('Received datagram:', decoded);59 }60 } catch (error) {61 console.error('Error reading datagrams:', error);62 }63 }6465 // Send a datagram66 async sendDatagram(data) {67 try {68 const encoded = new TextEncoder().encode(data);69 await this.datagramWriter.write(encoded);70 console.log('Datagram sent successfully');71 } catch (error) {72 console.error('Error sending datagram:', error);73 throw error;74 }75 }7677 // Create and handle a bidirectional stream78 async createBidirectionalStream() {79 try {80 const stream = await this.transport.createBidirectionalStream();81 const streamId = crypto.randomUUID();82 this.streams.set(streamId, stream);8384 // Handle incoming data85 this.handleStreamInput(stream, streamId);8687 return {88 streamId,89 writer: stream.writable.getWriter()90 };91 } catch (error) {92 console.error('Error creating bidirectional stream:', error);93 throw error;94 }95 }9697 async handleStreamInput(stream, streamId) {98 try {99 const reader = stream.readable.getReader();100 while (true) {101 const {value, done} = await reader.read();102 if (done) {103 console.log(`Stream ${streamId} reading complete`);104 break;105 }106 const decoded = new TextDecoder().decode(value);107 console.log(`Received on stream ${streamId}:`, decoded);108 }109 } catch (error) {110 console.error(`Error reading from stream ${streamId}:`, error);111 } finally {112 this.streams.delete(streamId);113 }114 }115116 // Send data through a specific stream117 async sendOnStream(streamId, data) {118 const stream = this.streams.get(streamId);119 if (!stream) {120 throw new Error(`Stream ${streamId} not found`);121 }122123 try {124 const writer = stream.writable.getWriter();125 const encoded = new TextEncoder().encode(data);126 await writer.write(encoded);127 await writer.close();128 console.log(`Data sent successfully on stream ${streamId}`);129 } catch (error) {130 console.error(`Error sending data on stream ${streamId}:`, error);131 throw error;132 }133 }134135 // Close the WebTransport connection136 async close() {137 try {138 await this.transport.close();139 console.log('Connection closed successfully');140 } catch (error) {141 console.error('Error closing connection:', error);142 throw error;143 }144 }145 }
Here's how to use the WebTransport implementation:
1 // Usage example2 async function main() {3 // Create a new transport client4 const client = new TransportClient('https://example.com/webtransport');56 try {7 // Connect to the server8 await client.connect();910 // Send a datagram11 await client.sendDatagram('Hello via datagram!');1213 // Create a bidirectional stream14 const {streamId, writer} = await client.createBidirectionalStream();1516 // Send data through the stream17 await client.sendOnStream(streamId, 'Hello via stream!');1819 // Close the connection when done20 await client.close();21 } catch (error) {22 console.error('Error:', error);23 }24 }
WebTransport Real-time Communication Demo
1 // server.js2 import { createServer } from "http";3 import { WebTransport } from "@fails-components/webtransport";45 const server = createServer();6 const port = 8080;78 // Create WebTransport server instance9 const wtServer = new WebTransport({10 port: port,11 host: "localhost",12 certificates: [] // Add your SSL certificates for production13 });1415 wtServer.on("session", async (session) => {16 console.log("New WebTransport session established");1718 // Handle bidirectional streams19 session.on("stream", async (stream) => {20 const reader = stream.readable.getReader();21 const writer = stream.writable.getWriter();2223 try {24 while (true) {25 const { value, done } = await reader.read();26 if (done) break;2728 // Echo received data back to client29 const response = `Server received: ${new TextDecoder().decode(value)}`;30 await writer.write(new TextEncoder().encode(response));31 }32 } catch (err) {33 console.error("Stream error:", err);34 } finally {35 reader.releaseLock();36 writer.releaseLock();37 }38 });39 });4041 server.listen(port, () => {42 console.log(`Server listening on port ${port}`);43 });4445 // client.js46 class WebTransportClient {47 constructor() {48 this.transport = null;49 this.stream = null;50 }5152 async connect() {53 try {54 this.transport = new WebTransport("https://localhost:8080/webtransport");55 await this.transport.ready;56 console.log("WebTransport connection established");5758 // Handle connection close59 this.transport.closed60 .then(() => console.log("Connection closed normally"))61 .catch((error) => console.error("Connection closed with error:", error));62 } catch (err) {63 console.error("Failed to establish WebTransport connection:", err);64 }65 }6667 async createStream() {68 try {69 this.stream = await this.transport.createBidirectionalStream();70 console.log("Bidirectional stream created");7172 // Set up stream reader73 this.startReading();74 return this.stream;75 } catch (err) {76 console.error("Failed to create stream:", err);77 }78 }7980 async startReading() {81 const reader = this.stream.readable.getReader();8283 try {84 while (true) {85 const { value, done } = await reader.read();86 if (done) break;8788 console.log("Received:", new TextDecoder().decode(value));89 }90 } catch (err) {91 console.error("Error reading from stream:", err);92 } finally {93 reader.releaseLock();94 }95 }9697 async sendMessage(message) {98 if (!this.stream) {99 console.error("No active stream");100 return;101 }102103 const writer = this.stream.writable.getWriter();104 try {105 await writer.write(new TextEncoder().encode(message));106 console.log("Message sent:", message);107 } catch (err) {108 console.error("Error sending message:", err);109 } finally {110 writer.releaseLock();111 }112 }113114 async close() {115 if (this.transport) {116 await this.transport.close();117 console.log("Connection closed");118 }119 }120 }121122 // Usage example123 async function main() {124 const client = new WebTransportClient();125126 // Connect to server127 await client.connect();128129 // Create bidirectional stream130 await client.createStream();131132 // Send test message133 await client.sendMessage("Hello WebTransport!");134135 // Close connection after 5 seconds136 setTimeout(async () => {137 await client.close();138 }, 5000);139 }140141 main().catch(console.error);
WebTransport Demo With Socket.Io
Here's a demo app showcasing real-time communication using the WebTransport API.
First, create a new directory, navigate into it, and initialize a Node.js project:
1 mkdir webtransport-socket-example2 cd webtransport-socket-example3 npm init -yp
Note: WebTransport can only function in a secure context (HTTPS), so even localhost isn't an exception. We need to generate an SSL certificate and key.
Read more about certificates - https://www.ssl.com/how-to/manually-generate-a-certificate-signing-request-csr-using-openssl/
Next, let's install a few packages:
Now, define the server code and its startup script in the package.json
file:
1 "main": "server.js",2 "scripts": {3 "start": "nodemon"4 },5 "type": "module",
Create a file server.js
with the following content:
1 import { readFileSync } from 'node:fs'2 import path from 'node:path'3 import { createServer } from 'node:https'4 import express from 'express'56 // Read SSL key and certificate7 const key = readFileSync('./key.pem')8 const cert = readFileSync('./cert.pem')910 // Create the Express app11 const app = express()12 // Serve `index.html` for all requests13 app.use('*', (req, res) => {14 res.sendFile(path.resolve('./index.html'))15 })1617 // Create the HTTPS server18 const httpsServer = createServer({ key, cert }, app)1920 const port = process.env.PORT || 4432122 // Start the server23 httpsServer.listen(port, () => {24 console.log(`Server listening at https://localhost:${port}`)25 })
Create a file index.html
with the following content:
1 <!DOCTYPE html>2 <html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>WebTransport</title>7 <link rel="icon" href="data:." />8 <script src="/socket.io/socket.io.js"></script>9 </head>10 <body>11 <h1>WebTransport</h1>12 <p>Connection: <span id="connection">Disconnected</span></p>13 <p>Transport: <span id="transport">Not Defined</span></p>14 </body>15 </html>
We have two paragraphs: one for the connection status and another for the data transport mechanism.
Run npm start
to start the development server. Navigate to https://localhost:3000
, accept the use of the self-signed certificate, and you’re ready to go!
Editing server.js
to Add WebSocket Support with Socket.IO
To enable WebSocket support on the server, make the following modifications:
1 // ...2 import { Server } from 'socket.io'34 // ...56 const io = new Server(httpsServer)78 // Handle connections9 io.on('connection', (socket) => {10 // Log the initial transport type: pooling, websocket, or webtransport (not yet available)11 console.log(`connected with transport ${socket.conn.transport.name}`)1213 // Handle transport upgrade: pooling → websocket → webtransport14 socket.conn.on('upgrade', (transport) => {15 console.log(`transport upgraded to ${transport.name}`)16 })1718 // Handle disconnections19 socket.on('disconnect', (reason) => {20 console.log(`disconnected due to ${reason}`)21 })22 })
Editing index.html
to Add WebSocket Support on the Client
Insert the following code before the </head>
tag to include the Socket.IO library:
1 <script src="/socket.io/socket.io.js"></script>
Add the following before the </body>
tag to handle WebSocket events:
1 <script>2 const $connection = document.getElementById('connection')3 const $transport = document.getElementById('transport')45 const socket = io()67 // Handle connection8 socket.on('connect', () => {9 console.log(`connected with transport ${socket.io.engine.transport.name}`)1011 $connection.textContent = 'Connected'12 $transport.textContent = socket.io.engine.transport.name1314 // Handle transport upgrade15 socket.io.engine.on('upgrade', (transport) => {16 console.log(`transport upgraded to ${transport.name}`)1718 $transport.textContent = transport.name19 })20 })2122 // Handle connection errors23 socket.on('connect_error', (err) => {24 console.log(`connect_error due to ${err.message}`)25 })2627 // Handle disconnections28 socket.on('disconnect', (reason) => {29 console.log(`disconnected due to ${reason}`)3031 $connection.textContent = 'Disconnected'32 $transport.textContent = 'Unavailable'33 })34 </script>
Restart the Server
Run the following command to restart the server:
1 npm start
Verify WebSocket Upgrade
Once the server is running, visit your application in the browser and verify that the transport is successfully upgraded to websocket
.
Next Step: Adding WebTransport Support
Extend the server.js
file to include WebTransport capabilities for enhanced transport options. Here's how to proceed:
1 // ...2 import { Http3Server } from '@fails-components/webtransport'34 // ...56 const io = new Server(httpsServer, {7 // `webtransport` must be explicitly specified8 transports: ['polling', 'websocket', 'webtransport'],9 })1011 // Create an HTTP/3 server12 const h3Server = new Http3Server({13 port,14 host: '0.0.0.0',15 secret: 'changeit',16 cert,17 privKey: key,18 })1920 // Start the HTTP/3 server21 h3Server.startServer();2223 // Create a stream and pass it to `socket.io`24 (async () => {25 const stream = await h3Server.sessionStream('/socket.io/')26 // Familiar processing logic27 const sessionReader = stream.getReader()2829 while (true) {30 const { done, value } = await sessionReader.read()31 if (done) {32 break33 }34 io.engine.onWebTransportSession(value)35 }36 })()
Editing index.html
Add the following to specify webtransport
as a transport option for Socket.IO:
1 <script>2 // ...34 const socket = io({5 transportOptions: {6 // Explicitly specify `webtransport`7 webtransport: {8 hostname: '127.0.0.1',9 },10 },11 })1213 // ...14 </script>
Restart the Server
Handling Certificate Issues
You might encounter an error related to an untrusted certificate. To resolve this, Chrome needs specific flags to handle HTTP/3 and QUIC protocols properly. After research, three Chrome flags are necessary:
--ignore-certificate-errors-spki-list
: Ignores SSL certificate errors for a specific certificate (requires the certificate's hash, see below).--origin-to-force-quic-on
: Forces QUIC protocol for specific origins.--user-data-dir
: Specifies a user profile data directory (required for reasons unclear).
Generating a Certificate Hash
Create a script called generate_hash.sh
to generate the certificate hash:
1 #!/bin/bash2 openssl x509 -pubkey -noout -in cert.pem |3 openssl pkey -pubin -outform der |4 openssl dgst -sha256 -binary |5 base64
Run the script:
This will produce the hash of your SSL certificate.
Launching Chrome with Necessary Flags
Create a script called open_chrome.sh to launch Chrome with the required flags:
1 #!/bin/bash2 google-chrome-stable \3 --ignore-certificate-errors-spki-list="<INSERT_HASH_HERE>" \4 --origin-to-force-quic-on="127.0.0.1:443" \5 --user-data-dir="/path/to/your/profile"
Replace <INSERT_HASH_HERE>
with the hash generated from generate_hash.sh
, and set the appropriate path for --user-data-dir
.
Important Notes:
Chrome Path Configuration
- To execute Chrome using the
chrome
command, ensure the path tochrome.exe
is included in your system's environment variable Path.
Example path:
C:\Program Files\Google\Chrome\Application\chrome.exe
Certificate Hash
The hash for --ignore-certificate-errors-spki-list
is the one generated earlier using the generate_hash.sh
script.
Run the script:
If you encounter the error chrome: command not found
, simply run the Chrome command directly in the terminal:
By following these steps, you should be able to launch Chrome with the necessary configurations to work with WebTransport over QUIC.
With these adjustments, your server and client should now support WebTransport via Socket.IO.
Conclusion
By following the steps outlined in this guide, you’ve successfully set up a WebTransport server with socket.io using Node.js and enabled support for the HTTP/3 (QUIC)
protocol in Google Chrome. From generating SSL certificates to handling WebTransport connections and configuring Chrome to trust your custom setup, you’ve explored a practical implementation of this modern protocol.
WebTransport is still an emerging technology, and while it’s not yet natively supported in Node.js, tools like @fails-components/webtransport
provide a way to experiment with its capabilities. With proper configurations and the flexibility of socket.io
, you can leverage the speed and efficiency of WebTransport for real-time data transfer in modern web applications.
Keep in mind that as WebTransport evolves, more stable and production-ready solutions will likely emerge, making it even easier to integrate this protocol into your projects. For now, this setup serves as an excellent starting point for experimenting with the future of real-time communication.