Introduction

In the age of real-time interactivity where services like ChatGPT excel, it’s crucial for developers to leverage technologies that allow for seamless data streaming in their applications. This article will delve into the world of HTML5 Server-Sent Events (SSE), a powerful tool akin to the technology behind conversational AI interfaces. Similar to how ChatGPT streams data to provide instant responses, SSE enables web browsers to receive updates from a server without the need for repetitive client-side requests. Whether you’re building a chat application, a live notification system, or any service requiring real-time data flow, this guide will equip you with the knowledge to implement SSE efficiently in your applications, ensuring a responsive and engaging user experience.

Understanding Server-Sent Events (SSE)

Server-Sent Events (SSE) is a web technology that facilitates the server’s ability to send real-time updates to clients over an established HTTP connection. Clients can receive a continuous data stream or messages via the EventSource JavaScript API, which is incorporated in the HTML5 specification by WHATWG. The official media type for SSE is text/event-stream.

Here is an illustrative example of a typical SSE response:

event:message
data:The Current Time Is 2023-12-30 23:00:21

event:message
data:The Current Time Is 2023-12-30 23:00:31

event:message
data:The Current Time Is 2023-12-30 23:00:41

event:message
data:The Current Time Is 2023-12-30 23:00:51

Fields in SSE Messages

Messages transmitted via SSE may contain the following fields:

  • event: A string specifying the event type. If designated, an event is dispatched in the browser to the corresponding event name listener. Use addEventListener() to listen for named events. The onmessage handler is invoked if no event name is specified.

  • data: This field contains the message content. If the EventSource receives multiple consecutive lines beginning with data:, it concatenates them, inserting a newline between each. Any trailing newlines are stripped.

  • id: An identifier to set the EventSource object’s last event ID value.

  • retry: The reconnection time in milliseconds. If the server connection drops, the browser will wait for this duration before attempting to reconnect. Non-integer values are disregarded.

Example Implementation

Let’s examine a simple example using the Go Gin SSE to demonstrate SSE functionality.

Client-Side Code

On the client side, only a minimal amount of HTML and JavaScript is necessary.

<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Server Sent Event Example</title>
</head>

<body>
<div id="event-data"></div>
<script>
    // The EventSource object in JavaScript listens for streaming events from our Go server and displays the messages.
    const stream = new EventSource("/stream");
    stream.onmessage = function (e) {
        // Append the message data to the event-data div
        const eventDataElement = document.getElementById('event-data');
        eventDataElement.innerHTML += e.data + "<br>";
    };
</script>

</body>
</html>

Explanation

  • The EventSource object automatically establishes a connection to the server endpoint /stream, which should be set up to send continuous events.
  • An event listener for the “message” event is added to the stream object to process incoming messages.
  • The data from the event (e.data) is appended to the content of the event-data div, with each message on a new line.

Server-Side Code

The essential server logic, with Go channels, allows for the management of multiple clients.

// Middleware to set the necessary headers for SSE
func HeadersMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Content-Type", "text/event-stream")
        c.Writer.Header().Set("Cache-Control", "no-cache")
        c.Writer.Header().Set("Connection", "keep-alive")
        c.Writer.Header().Set("Transfer-Encoding", "chunked")
        c.Next()
    }
}

// Endpoint to stream messages to the client
authorized.GET("/stream", HeadersMiddleware(), func(c *gin.Context) {
    clientChan, ok := c.Get("clientChan")
    if !ok {
        return
    }
    c.Stream(func(w io.Writer) bool {
        if msg, ok := <-clientChan; ok {
            c.SSEvent("message", msg)
            return true
        }
        return false
    })
})

Let’s explore the SSEvent method in the Gin framework:

// SSEvent writes a Server-Sent Event into the body stream.
func (c *Context) SSEvent(name string, message any) {
    c.Render(-1, sse.Event{
        Event: name,
        Data:  message,
    })
}

// Event struct for SSE
type Event struct {
    Event string
    Id    string
    Retry uint
    Data  interface{}
}

Addressing Challenges

While the client and server code for SSE appears straightforward, constructing a production-level SSE application introduces complexities. The JavaScript SSE API has certain limitations:

  • Only supports the GET method.
  • Cannot send a request body.
  • Does not allow custom headers, which can be crucial for passing authentication tokens.
  • Limited control over retry strategy, with the browser attempting reconnection a few times before ceasing.

Solutions for Enhanced Flexibility

To overcome the constraints of the JavaScript EventSource API, consider utilizing third-party libraries such as fetch-event-source from Azure. This library leverages the Fetch API to manage server-sent events, offering greater control over request methods, headers, and bodies.

Example usage of the fetch-event-source library:

// Using the fetch-event-source library for more flexibility
import { fetchEventSource } from '@microsoft/fetch-event-source';

const ctrl = new AbortController();

fetchEventSource('/api/sse', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-access-token'
    },
    body: JSON.stringify({ foo: 'bar' }),
    signal: ctrl.signal,
    onmessage(ev) {
        console.log(ev.data);
    },
    onopen(response) {
        // Handle the open event
    },
    onerror(err) {
        // Handle errors
    },
    onclose() {
        // Handle the close event
    }
});

Conclusion

While using SSE and the EventSource API for HTTP streaming is intuitive and straightforward, the limited customization options can hinder the development of comprehensive production applications. Employing third-party EventSource implementations can provide the necessary flexibility and control required for robust applications.

References