Skip to content

Progress Reporting in Web Application using Server‐Sent Events (SSE)

adriancs edited this page Aug 2, 2025 · 7 revisions

Part 4: Progress Reporting in Web Application using Server-Sent Events (SSE)

Main Content:


Server-Sent Events (SSE), a single constant light weight connection that allows the Backend to have real-time communication with the Frontend. This is a one way communication, but it's very excellent and especially effective in dealing with this kind of progress reporting task that constantly provides updates status.

Handling SSE at the Backend

We're presenting the tutorial in Vanilla ASP.NET Web Forms, and again, it's core logic and patterns can be easily migrated to other Web Applications, such as: MVC, .NET Core, etc...

Vanilla ASP.NET Web Forms - Zero ViewState, No Server Control, No Custom User Control, No UpdatePanel, No GridView. Just pure HTML, JavaScript and CSS

As you have already approaching the part 4 of the series, to create an API page.... you know the drill...

Create a new blank Web Forms page, which again, looks like this:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
        </div>
    </form>
</body>
</html>

and again, delete all Frontend markup, leave only the first line, the page directive declaration:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>

Now, the next is a crucial step. When handling SSE in ASP.NET Web Forms, the system will lock the page for only allowing one session to connect to it at a time.

All the three actions Backup, Restore and Stop are still required to use HTTP reqeust to the Backend API to initiate the process.

Backup and Restore can most probably started without issue. Then followed by the SSE connection. Once the SSE establish connection, we can no longer send any HTTP request to the same page... until the SSE closes it's connection.

But however, we still need to allow the Stop request to be sent to the API Backend handling.

Therefore, we need to tell the page for not locking the session, by using "EnableSessionState="ReadOnly" at the page directive:

<%@ Page Language="C#" EnableSessionState="ReadOnly" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>

Continue to the code behind...

A typical boilet plate of C# Backend code structure. The entry point is divided into 2 sections.

  • Section 1: Handling SSE
  • Section 2: Handling Normal API Request
public partial class apiProgressReport4_ServerSentEvent : System.Web.UI.Page
{
    static ConcurrentDictionary<int, TaskInfo5> dicTaskInfo = new ConcurrentDictionary<int, TaskInfo5>();

    protected void Page_Load(object sender, EventArgs e)
    {
        // The main entry point
    
        // --------------------------------------
        // Section 1: Handling Server-Sent Events
        // --------------------------------------
        
        // Check if this is an Server-Sent Events request
        if (Request.Headers["Accept"] == "text/event-stream" || Request.QueryString["stream"] == "true")
        {
            HandleSSERequest();
            return;
        }

        // --------------------------------------
        // Section 2: Handling Normal API Request
        // --------------------------------------
        
        // Serves as API Endpoint

        try
        {
            string action = (Request["action"] + "").Trim().ToLower();

            switch (action)
            {
                case "start_backup":
                    StartBackup();
                    break;
                case "start_restore":
                    StartRestore();
                    break;
                case "stop_task":
                    Stop();
                    break;
                case "":
                    // HTTP Error 400 Bad Request
                    Response.StatusCode = 400;
                    Response.Write("Empty request");
                    break;
                default:
                    // HTTP Error 405 Method Not Allowed
                    Response.StatusCode = 405;
                    Response.Write($"Action not supported: {action}");
                    break;
            }
        }
        catch (Exception ex)
        {
            // HTTP Error 500 Internal Server Error
            Response.StatusCode = 500;
            Response.Write(ex.Message);
        }
    }
}

The task of Backup, Restore and Stop will be same as we discussed before in the previous series (Part 2, Part 3), we'll directly jump to the SSE handling.

After detecting the SSE connection request at the Page_Load event:

if (Request.Headers["Accept"] == "text/event-stream" || Request.QueryString["stream"] == "true")
{
    HandleSSERequest();
    return;
}

We'll route the request to a dedicated method: HandleSSERequest():

private void HandleSSERequest()
{
    // as usual, check user login authentication as normal
    if (!IsUserAuthenticated())
    {
        Response.StatusCode = 401;
        Response.Write("Unauthorized");
        
        // close the SSE request
        Response.End();
        return;
    }

    // In SSE connection, the Frontend can only sent the task id through Query String
    // example: /apiBackup?taskid=1
    
    // attempt to get the task id from the Query String
    if (!int.TryParse(Request["taskid"] + "", out int taskId))
    {
        Response.StatusCode = 400;
        Response.Write("Invalid or missing taskid parameter");
        Response.End();
        return;
    }

    // Set SSE headers
    Response.ContentType = "text/event-stream";
    Response.CacheControl = "no-cache";
    Response.AddHeader("Connection", "keep-alive");
    Response.AddHeader("Access-Control-Allow-Origin", "*");
    Response.AddHeader("Access-Control-Allow-Headers", "Cache-Control");
    Response.Buffer = false;

    try 
    {
        // Event name: "connected"
        // Send initial connection event
        SendSSEEvent("connected", $"Subscribed to task {taskId}");

        // Perform a loop
        while (Response.IsClientConnected)
        {
            // Get the task info from the global distionary
            var taskInfo = GetTaskInfoObject(taskId);

            if (taskInfo != null)
            {
                // Event name: "progress"
                // Send progress update for active task, formatted as JSON string
                SendSSEEvent("progress", JsonSerializer.Serialize(taskInfo));

                // If task is completed, send final update and close
                if (taskInfo.IsCompleted)
                {
                    // Event name: "completed"
                    SendSSEEvent("completed", JsonSerializer.Serialize(taskInfo));

                    // Terminate the loop
                    break;
                }
            }

            // Repeat every 250 milliseconds
            Thread.Sleep(250); 
        }
    }
    catch (HttpException)
    {
        // Client disconnected
    }
    finally
    {
        Response.End();
    }
}

// A helper method to send the data to the Frontend
private void SendSSEEvent(string eventType, string data)
{
    try
    {
        if (!Response.IsClientConnected)
            return;

        var eventData = new StringBuilder();

        if (!string.IsNullOrEmpty(id))
            eventData.AppendLine($"id: {id}");

        eventData.AppendLine($"event: {eventType}");
        eventData.AppendLine($"data: {data}");
        eventData.AppendLine(); // Empty line required by SSE spec

        Response.Write(eventData.ToString());
        Response.Flush();
    }
    catch (HttpException)
    {
        // Client disconnected
    }
}

That is all for the Backend.

Now, let's move to the Frontend - JavaScript:

// caching all the UI value containers

// task related global variables
let urlApiEndpoint = "/apiBackup";
let currentTaskId = 0;
let eventSource = null;
let isConnecting = false;

// The main function to open and handle the communication of SSE
function connectSSE(taskId) {

    // The connection is establishing... do nothing... return
    if (isConnecting || (eventSource && eventSource.readyState === EventSource.OPEN)) {
        return;
    }

    // Set a boolean to the page a connection attempt is underway
    isConnecting = true;

    try {
        // Set the task id in the Query String
        const sseUrl = `${urlApiEndpoint}?stream=true&taskId=${taskId}`;
        
        // Establish the SSE connection
        eventSource = new EventSource(sseUrl);

        // This happens when the connection is handled/accepted by the Backend
        eventSource.onopen = function (event) {
            console.log('SSE connected');
            isConnecting = false;
        };

        // Handle different event types
        eventSource.addEventListener('connected', function (event) {
            console.log('SSE subscription:', event.data);
        });

        eventSource.addEventListener('progress', function (event) {
            try {
                
                // Convert the JSON formatted string into JSON object
                const jsonObject = JSON.parse(event.data);
                
                // Then, update the UI
                updateUIValues(jsonObject);
                
            } catch (err) {
                console.error('Error parsing SSE progress data:', err);
            }
        });

        eventSource.addEventListener('completed', function (event) {
            try {
            
                // Done
                
                // Convert the JSON formatted string into JSON object
                const jsonObject = JSON.parse(event.data);
                
                // Then, update the UI
                updateUIValues(jsonObject);
                
                showGoodMessage("Task Completed", "Task finished successfully");
                
                // Close the SSE connection
                closeSSE();
                
            } catch (err) {
                console.error('Error parsing SSE completion data:', err);
            }
        });

        // Server-Sent Events of "error" - this is manually defined at the Backend
        eventSource.addEventListener('error', function (event) {
            showErrorMessage("SSE Error", event.data || "Connection error");
            closeSSE();
        });
        
        eventSource.onerror = function (event) {
            console.error('SSE error:', event);
            isConnecting = false;

            if (eventSource.readyState === EventSource.CLOSED) {
                showErrorMessage("Connection Error", "SSE connection failed");
                enableButtons();
            }
        };

    } catch (err) {
        console.error('Failed to create SSE connection:', err);
        isConnecting = false;
        showErrorMessage("Connection Error", "Failed to establish SSE connection");
        enableButtons();
    }
}

// Close the SSE
function closeSSE() {
    if (eventSource) {
        eventSource.close();
        eventSource = null;
    }
    isConnecting = false;
    enableButtons();
}

// Close the SSE connection
window.addEventListener('beforeunload', function () {
    closeSSE();
});

The basic JavaScript FetchAPI code has already been introduced in previous series (Part 2, Part 3), they are well documented. In this document, we'll assume you have already well aware of the FetchAPI by now, so we'll just quickly skip those part and jump right into the SSE related code.

Now, we have already build the core function for SSE, the integration part will be very easy.

async function startBackup() {
 
    // build the form data
    
    // send the HTTP request to the Backend API
    
    // get the task id
    
    // start the SSE connection, updating the UI with progress status
    connectSSE(currentTaskId);

}

async function startRestore() {
    
    // do validation of the file upload
    
    // build the form data
    
    // send the HTTP request to the Backend API
    
    // get the task id
    
    // begin the SSE, start updating the UI with progress status values
    connectSSE(currentTaskId);
 }

And just like that you have a simple, efficient and real-time progress reporting updates.

And that's it guys. I'll see you around and happy coding.

We have developped a comprehensive and robust Server-Sent Events demo, you may view the source code at the following files:

Reference

Clone this wiki locally