Edit

Share via


Write an upstream server for Azure Web PubSub

Azure Web PubSub routes system and user events to an upstream server so that you can authorize clients and handle events from the clients. This article explains how the service calls upstream handlers, then shows complete implementations in C#, JavaScript, Java, and Python, followed by debugging tips.

Prerequisites

Understand how Azure Web PubSub calls upstreams

Azure Web PubSub forwards events to the HTTP endpoint you configure per hub. Each callback arrives as a CloudEvent that contains metadata such as the event type (ce-type), the user identity (ce-userid), and the request body. The service expects:

  • A reachable endpoint defined in the hub's event handler settings (for example, tunnel:///eventhandler when using the tunnel tool, or https://contoso.com/eventhandler when your upstream server endpoint is exposed and reachable).
  • connect responses that return userId, initial groups to join, and other negotiated properties as described in the connect event spec.
  • Standard HTTP status codes. Returning 401 or 403 from connect rejects the client, while returning 2xx from user events acknowledges the message.

Keep this contract in mind while implementing and testing your upstream logic.

Implement the upstream server

The upstream server processes events that the service forwards to any reachable HTTP endpoint. You can expose your handler publicly (for example, https://contoso.com/eventhandler) or keep it private behind the Web PubSub local tunnel tool while developing locally. The examples below use the tunnel endpoint for convenience, but the handler logic works the same no matter how it's hosted.

Configure the hub to call your upstream

Important

Replace <your-unique-resource-name> with the name of your Azure Web PubSub resource and keep the hub name that matches your app. When deploying to a publicly reachable server, set url-template to the fully qualified URL (for example, https://contoso.com/eventhandler). When testing locally through the tunnel, use tunnel:///eventhandler as shown below.

az webpubsub hub create \
    -n "<your-unique-resource-name>" \
    -g "myResourceGroup" \
    --hub-name "Sample_ChatApp" \
    --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connect" system-event="connected" system-event="disconnected"

Sample upstream implementations

The upstream server handles events based on your event handler configuration. The examples below demonstrate handling common events:

  • connect events to validate clients and assign a userId.
  • connected events to log successful joins.
  • disconnected events to capture why clients leave and clean up the resources.
  • User events (for example, message) that carry user payloads so your upstream can run your application logic.

You can configure which system events and user event patterns the service forwards to your upstream by adjusting the --system-event and --user-event-pattern parameters in your hub settings.

Each language sample below expects the upstream to listen on port 8080 at /eventhandler.

Note

The tunnel tool embeds a userId inside each client’s SAS token. Azure Web PubSub passes this value to your upstream through the ce-userId CloudEvent header (surfaced as ConnectionContext.UserId in the .NET and JavaScript helpers). The samples below read that header whenever it exists and only generate random IDs for demonstration. Replace that fallback with your production authentication logic.

Prerequisites

  • .NET 8 SDK installed.

Initialize the project

dotnet new web -n SampleChatApp
cd SampleChatApp

Install packages

dotnet add package Microsoft.Azure.WebPubSub.AspNetCore

Add the code

Replace the contents of Program.cs with the following single-file sample. It builds and runs the upstream handler on port 8080.

using System;
using Azure.Core;
using Azure.Messaging.WebPubSub;
using Microsoft.Azure.WebPubSub.AspNetCore;
using Microsoft.Azure.WebPubSub.Common;

var connectionString = Environment.GetEnvironmentVariable("WebPubSubConnectionString")
    ?? throw new InvalidOperationException("Set the WebPubSubConnectionString environment variable.");

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddWebPubSub(o => o.ServiceEndpoint = new WebPubSubServiceEndpoint(connectionString))
    .AddWebPubSubServiceClient<Sample_ChatApp>();

var app = builder.Build();

app.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");

app.Run("http://localhost:8080");

sealed class Sample_ChatApp : WebPubSubHub
{
    private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;

    public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
    {
        _serviceClient = serviceClient;
    }

    public override ValueTask<ConnectEventResponse> OnConnectAsync(
        ConnectEventRequest request,
        CancellationToken cancellationToken)
    {
        var userId = request.ConnectionContext.UserId;
        if (string.IsNullOrEmpty(userId))
        {
            // Demo only: assign a random ID when the tunnel doesn't include one in the SAS token.
            // Production apps should authenticate callers and issue their own stable user IDs.
            userId = Guid.NewGuid().ToString();
            Console.WriteLine($"[CONNECT] Generated demo user id: {userId}");
        }
        else
        {
            Console.WriteLine($"[CONNECT] Received user id: {userId}");
        }

        return new ValueTask<ConnectEventResponse>(
            request.CreateResponse(userId: userId, null, null, null));
    }

    public override Task OnConnectedAsync(ConnectedEventRequest request)
    {
        Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
        return Task.CompletedTask;
    }

    public override Task OnDisconnectedAsync(DisconnectedEventRequest request)
    {
        Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} disconnected: {request.Reason}");
        return Task.CompletedTask;
    }

    public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(
        UserEventRequest request,
        CancellationToken cancellationToken)
    {
        await _serviceClient.SendToAllAsync(
            RequestContent.Create(new
            {
                from = request.ConnectionContext.UserId,
                message = request.Data.ToString()
            }),
            ContentType.ApplicationJson);

        return new UserEventResponse();
    }
}

Run the upstream

set WebPubSubConnectionString=<your connection string>   # use export on macOS/Linux
dotnet run

Debug event handlers

The tunnel lets you keep a local server private while still receiving callbacks from Azure Web PubSub. Start it alongside your upstream to inspect CloudEvents end to end. It also provides a way to establish a client connection to Web PubSub easily so you can try the end-to-end flow in the tunnel web page.

Tip

See Azure Web PubSub local tunnel tool for installation, authentication, and troubleshooting guidance.

export WebPubSubConnectionString="<your connection string>"
awps-tunnel run --hub Sample_ChatApp --upstream http://localhost:8080 --verbose
  • Open the web view URL that the tunnel prints (for example, http://127.0.0.1:9080). The Server tab shows requests flowing to your upstream, while the Client tab lets you establish WebSocket connections.
  • Use the Client tab’s Connect button (or copy the connection URL it displays) to spin up client connections.

Inspect CloudEvents and service telemetry

  • Log ce-type, ce-userid, and query parameters inside your upstream code to see which phase (connect, connected, disconnected, user event) triggered the callback.
  • Return descriptive HTTP status codes (for example, 401 for missing auth or 400 for malformed input). The tunnel UI and service diagnostics surface these failures immediately.
  • Use Azure Web PubSub metrics and Live Trace in the portal to confirm whether events reach the upstream when debugging in production or other remote environments.

Next steps