Nowadays, push notifications are a must have feature in every modern web / mobile application.
Real time update notifications, asynchronous/long running task notification are great use cases for this feature. As an IT guy, you’ve probably tried or have implemented this feature in an application. If you haven’t, you’ve certainly asked yourself the following question: how this can be done ?
Answer : There is different ways, each way has its advantages and drawbacks.
First method : XHR Polling
This method consists of making a repetitive HTTP call after certain amount of time in order to retrieve updates.
Advantages: simple to implement / debug, compatible with all browsers and architectures.
Drawbacks: one way communication,inefficient and resource wasting (some calls may return an empty results since there is no update is made).
Second method: SSE events
The second way consists of opening a persistent HTTP connection between the client and the server. When a change is made, the server sends data in which we call a Server Sent Event (SSE) to the client.
Advantages: Native implementation in JS (EventSource), supported reconnection and state reconciliation mechanisms
Drawbacks: One way communication, uses a persistent connection
Third method: Websockets
WebSocket is a full-duplex protocol allowing a bidirectional communication between the server and the client.
Advantages: full duplex communication
Drawbacks: Long lived connections between the server and the client, no support for reconnection and state reconciliation.
The real dilemma is the persistent connections which is not always possible with serverless plateformes and technologies using short lived connection.
So, how can we achieve the same goal with a fancy solution ?
Answer : The Mercure protocol.
What is Mercure ?
Mercure is a protocol allowing to push data updates to web browsers and other HTTP clients in a convenient, fast, reliable and battery-efficient way. It is especially useful to publish real-time updates of resources served through web APIs, to reactive web and mobile apps.
Among the advantages of this protocol :
- Native browser support ;
- Compatible with all existing servers and can work with old browsers (IE7+) using an EventSource polyfill ;
- Built-in connection re-establishment and state reconciliation ;
- JWT-based authorization mechanism (securely dispatch an update to some selected subscribers) ;
- Message encryption support ;
More details can be found in the official website : https://mercure.rocks/
Key concepts
After this brief introduction, let’s dive into the involved components of this protocol.
The first component is the Topic which is the unit we publish and subscribe to.
The publisher: is responsible of sending updates to the hub, he is also able to securely dispatch update to specific targets.
The subscriber: can be a server/client side application that subscribes to real-time updates from the hub.
The famous Hub: is a server that handles subscription requests and distributes the content to subscribers when the corresponding topics have been updated.
And last but not least, their is the target(s) which can be a subscriber, or a group of subscribers.
Now, after you’ve had an idea about the components, let’s see how they communicate each other.
Publishing :
In order to send a message to a client application, the publisher issues a POST request to the hub which afterwards dispatches the message to the subscriber(s) using a SSE.
The request must be encoded using the application/x-www-form-urlencoded format.
The request body should contain at least the following data :
- topic: the name of the topic which will receive the message.
- data: contains the content of the message.
In order to dispatch private updates, we can add the topic parameter to the request body which we contain the target(s) allowed to receive the update.
The publisher must present a valid JWT that contains a claim named “mercure”, this claim must contain a “publish” key which is an array of the authorized targets to dispatch to.
VERY IMPORTANT :
The value of “mercure.publish” determines the capabilities of the publisher.
if “mercure.publish”:
- is not defined, then the publisher is not allowed to dispatch any update ;
- contains an empty array, then the publisher is only allowed to dispatch public updates ;
- contains the reserved string * as an array value, then the publisher is authorized to dispatch updates to all targets ;
Subscribing :
The subscriber / client subscribes to the hub URL in order to receive updates by using a GET request that contains the topic names to get updates from.
A subscriber may need to be authorized to receive updates destined to specific targets. To receive these specific updates, the JWS presented by the subscriber must have a claim named mercure with a key named subscribe that contains an array of strings ;
Authorization :
In order to ensure that both publishers / subscribers are authorized for private updates, a JWS (JSON Web Signature) must be provided ;
There are 2 mechanisms to present the JWS to the hub :
Using an Authorization HTTP header :
- Used if the publisher / subscriber is not a web browser.
- The header contains a string Bearer followed by the JWS.
Using a cookie :
- Used if the publisher / subscriber is a web browser.
- The browser should send a cookie named mercureAuthorization which contain the JWS.
When using authorization mechanisms, the connection MUST use an encryption layer such as HTTPS;
Reconnection & State Reconciliation
The connection between the subscriber can be lost at any time and the user may not receive notifications about the changes that happened during that time.
To avoid that, the subscriber should send the id of the last received update. This id should be sent from the hub and must be a Global unique identifier (i.e: GUID, UUID, …).
During the reconnection, the subscriber will automatically re-connect to the hub (according to the SSE specifications).
During this phase, the ID should be sent in Last-Event-ID HTTP Header. It can be also provided as a query parameter (with the same name) during the discovery in order to fetch any update dispatched between the initial resource generation by the publisher and the connection to the hub.
If both HTTP Header and the query parameter are provided, the HTTP Header takes the precedence.
Encryption
Relaying on HTTPS as an encryption is not totally secure, since the hub can be managed by a service provider and anyone who has access to the hub can see the content of all messages.
To ensure a total privacy, the message must be encoded by the publisher before sending it to the publisher using Json Web Encryption. The subscriber must have knowledge of this key in order to decrypt the message. The exchange can be done by any relevant mechanism.
A possible way of that is to send an encoded key in the key-set attribute during the discovery.
I hope that it wasn’t borring for you and you’ve got a clear idea about the Mercure protocol.
You can check an example in my github repository where i used a Symfony backend as a publisher and a React.js web app as a subscriber.