Skip to content

Endpoints

Pode has support for upgrading regular HTTP requests, made to a Route, to WebSocket (Signal) connections. This allows you to stream events/messages from your server to one or more connected clients, and vice-versa from clients to the server. Connections can be scoped to just the Route that converted the request and it will be closed at the end of the Route like a normal request flow (Local), or you can keep the connection open beyond the request flow and be used server-wide for sending events and messages (Global).

WebSocket connections are typically made from client browsers via JavaScript, using the WebSocket class. But they can also be made via other languages such as .NET, Java, etc. Examples on this page will use JavaScript.

Note

The maximum size for WebSocket payloads in Pode depends on the version of PowerShell being used:

  • PowerShell <= 6.0, the maximum payload size is 32KB.
  • PowerShell >= 7.0, the maximum payload size is 16KB.

If a message exceeds these limits, the connection may be closed or the message may not be delivered correctly.

Important

For backwards compatibility the default upgrade path for WebSockets is to "auto-upgrade" when a valid HTTP request, with a Sec-WebSocket-Key header, is sent to the server. There is now a manual upgrade path, via ConvertTo-PodeSignalConnection, which can be used to align with SSE connections.

The default auto-upgrade logic should now be considered deprecated, and will eventually be removed. This page will assume the manual approach, but will reference the legacy auto-upgrade approach as well.

Server Side

Listening

The first thing you'll need to do to enable your server to listen for WebSocket requests, is to setup an Endpoint via Add-PodeEndpoint with a protocol of either Ws or Wss. This Endpoint will later be used by Pode to receive, send, and parse WebSocket messages once the initial HTTP request has be upgraded.

Add-PodeEndpoint -Address * -Port 8091 -Protocol Ws -NoAutoUpgradeWebSockets

# or for secure sockets:
Add-PodeEndpoint -Address * -Port 8091 -Certificate './path/cert.pfx' -CertificatePassword 'dummy' `
    -Protocol Wss NoAutoUpgradeWebSockets

Note

For those using the legacy auto-upgrade approach, you'll need to omit -NoAutoUpgradeWebSockets.

Convert Request

To convert a request into a WebSocket connection use ConvertTo-PodeSignalConnection. This will automatically send back the appropriate HTTP response headers to the client, converting it into a WebSocket connection; allowing the connection to be kept open, and for messages to be streamed back to the client - and vice-versa. A -Name must be supplied during the conversion, allowing for easier reference to all connections later on, and allowing for different connection groups (of which, you can also have -Group within a Name as well).

For example, any requests to the following Route will be converted to a globally scoped WebSocket connection, and be available under the ResponseTimes name:

Add-PodeRoute -Method Get -Path '/response-times' -ScriptBlock {
    ConvertTo-PodeSignalConnection -Name 'ResponseTimes'
}

You could then use Send-PodeSignal in a Schedule (more info below) to broadcast a message, every minute, to all connected clients within the ResponseTimes name:

Add-PodeSchedule -Name 'Example' -Cron (New-PodeCron -Every Minute) -ScriptBlock {
    Send-PodeSignal -Name 'ResponseTimes' -Data @{ Durations = @(123, 101, 104) }
}

Once ConvertTo-PodeSignalConnection has been called, the $WebEvent object will be extended to include a new Signal property. This new property will have the following items:

Name Description
Name The Name given to the connection
Group An optional Group assigned to the connection within the Name
ClientId The assigned ClientId for the connection - this will be different to a passed ClientId if using signing
IsLocal Is the connection Local
IsGlobal Is the connection Global

Therefore, after converting a request, you can get the client ID back via:

Add-PodeRoute -Method Get -Path '/response-times' -ScriptBlock {
    ConvertTo-PodeSignalConnection -Name 'ResponseTimes'
    $clientId = $WebEvent.Signal.ClientId
}

Tip

The Name, Group, and Client ID values are also sent back on the HTTP response during conversion as headers. These won't be available if you're using JavaScript's WebSocket class, but could be if using other WebSocket libraries. The headers are:

  • X-PODE-SIGNAL-CLIENT-ID
  • X-PODE-SIGNAL-NAME
  • X-PODE-SIGNAL-GROUP

ClientIds

ClientIds created by ConvertTo-PodeSignalConnection will be a GUID by default however, you can supply your own IDs via the -ClientId parameter:

Add-PodeRoute -Method Get -Path '/response-times' -ScriptBlock {
    $clientId = Get-Random -Minimum 10000 -Maximum 999999
    ConvertTo-PodeSignalConnection -Name 'ResponseTimes' -ClientId $clientId
}

You can also sign clientIds as well.

Scopes

The default scope for a new WebSocket connection is "Global", which means the connection will be stored internally and can be used outside of the converting Route to stream messages back to the client.

The default scope for new WebSocket connections can be altered by using Set-PodeSignalDefaultScope. For example, if you wanted all new WebSocket connections to instead default to a Local scope:

Set-PodeSignalDefaultScope -Scope Local
Global

A Globally scoped WebSocket connection is the default (unless altered via Set-PodeSignalDefaultScope). A Global connection has the following features:

  • They are kept open, even after the Route that converted the request has finished.
  • The connection is stored internally, so that messages can be streamed to the clients from other Routes, Timers, etc.
  • You can send messages to a specific connection if you know the Name and ClientId for the connection.
  • Global connections can be closed via Close-PodeSignalConnection.

For example, the following will convert requests to /response-times into global WebSocket connections, and then a Schedule will send messages to them every minute:

Start-PodeServer {
    Add-PodeEndpoint -Address * -Port 8091 -Protocol Http
    Add-PodeEndpoint -Address * -Port 8081 -Protocol Ws -NoAutoUpgradeWebSockets

    Add-PodeRoute -Method Get -Path '/response-times' -ScriptBlock {
        ConvertTo-PodeSignalConnection -Name 'ResponseTimes'
    }

    Add-PodeSchedule -Name 'Example' -Cron (New-PodeCron -Every Minute) -ScriptBlock {
        Send-PodeSignal -Name 'ResponseTimes' -Data @{ Durations = @(123, 101, 104) }
    }
}
Local

A Local connection has the following features:

  • When the Route that converted the request has finished, the connection will be closed - the same as HTTP requests.
  • The connection is not stored internally, it is only available for the lifecycle of the HTTP request.
  • You can send messages back to the connection from within the converting Route's scriptblock, but not from Timers, etc. When sending messages back for local connections you'll need to supply the Name of the connection to Send-PodeSignal, this will automatically detect it's a local connection and use the socket via the current $WebEvent.

For example, the following will convert requests to /response-times into local WebSocket connections, and two messages will be sent back to the client before the connection is closed:

Start-PodeServer {
    Add-PodeEndpoint -Address * -Port 8091 -Protocol Http
    Add-PodeEndpoint -Address * -Port 8081 -Protocol Ws -NoAutoUpgradeWebSockets

    Add-PodeRoute -Method Get -Path '/response-times' -ScriptBlock {
        ConvertTo-PodeSignalConnection -Name 'ResponseTimes' -Scope Local
        Send-PodeSignal -Name 'ResponseTimes' -Data @{ Durations = @(123, 101, 104) }
        Start-Sleep -Seconds 10
        Send-PodeSignal -Name 'ResponseTimes' -Data @{ Durations = @(234, 202, 205) }
    }
}

Send Messages

To send a message from the server to one or more connected clients, you can use Send-PodeSignal. Using the -Data parameter, you can either send a raw string value, or a more complex hashtable/psobject which will be auto-converted into a JSON string.

For example, to broadcast a message to all clients on a "ResponseTimes" Signal connection:

# simple string
Send-PodeSignal -Name 'ResponseTimes' -Data 'Times: 123, 101, 104'

# complex object
Send-PodeSignal -Name 'ResponseTimes' -Data @{ ResponseTimes = @(123, 101, 104) }

Or to send a message to a specific client:

Send-PodeSignal -Name 'ResponseTimes' -ClientId 'some-client-id' -Data @{ ResponseTimes = @(123, 101, 104) }

Note

For those using the legacy auto-upgrade approach, the -Name will be the URI path your WebSocket connected to. For example, if you connected to ws://localhost:8080/messages then you'll pass /messages to -Name.

Routes

When a client sends a message back to the server on the connected WebSocket, Pode will automatically call Send-PodeSignal to re-broadcast the message back to all clients - or to a specific Path/ClientId if supplied by the sending client.

However, you can add custom routing logic for WebSocket paths using Add-PodeSignalRoute. This is much like Add-PodeRoute, but allows you to run custom logic on paths for messages sent by clients. When you use a custom Signal Route, it is responsible for calling Send-PodeSignal.

Also like Add-PodeRoute there is a $SignalEvent object that you can use, which contains the client's message data, the raw Request/Response objects, etc.

For example, the following Signal Route will broadcast the current date back to all clients connected to the Date WebSocket, when a client sends the message [date] on the /date WebSocket path:

Start-PodeServer {
    Add-PodeEndpoint -Address * -Port 8091 -Protocol Http
    Add-PodeEndpoint -Address * -Port 8081 -Protocol Ws -NoAutoUpgradeWebSockets

    Add-PodeRoute -Method Get -Path '/date' -ScriptBlock {
        ConvertTo-PodeSignalConnection -Name 'Date'
    }

    Add-PodeSignalRoute -Path '/date' -ScriptBlock {
        if ($SignalEvent.Data.Message -ieq '[date]') {
            Send-PodeSignal -Name 'Date' -Data ([datetime]::Now.ToString())
        }
    }
}

Signal Event

When using custom Signal Routes the $SignalEvent is a HashTable that is available for you to use - much like the $WebEvent object for normal Routes.

This $SignalEvent object has the following properties:

Name Type Description
Data hashtable Contains the Message, and an optional Path/ClientId/Group to broadcast back to
Endpoint hashtable Contains the Address/Protocol of the endpoint being hit - such as "pode.example.com"/"127.0.0.2", or WS/WSS for the Protocol
Lockable hashtable A synchronized hashtable that can be used with Lock-PodeObject
Path string The path of the WebSocket - such as "/messages"
Request object The raw Request object
Response object The raw Response object
Route hashtable The current Signal Route that is being invoked
Streamed bool Specifies whether the current server type uses streams for the Request/Response, or raw strings
Timestamp datetime The current date and time of the Signal

Broadcast Levels

By default, Pode will allow the broadcasting of messages to all clients for a WebSocket connection Name, Group, or a specific ClientId.

You can supply a custom broadcasting level for specific WebSocket connection names (or all), limiting broadcasting to requiring a specific ClientId for example, by using Set-PodeSignalBroadcastLevel. If a -Name is not supplied then the level type is applied to all WebSocket connections.

For example, the following will only allow messages to be broadcast to a WebSocket connection name if a ClientId is also specified on Send-PodeSignal - preventing accidentally broadcasting to every connected client:

# apply to all WebSocket connections
Set-PodeSignalBroadcastLevel -Type 'ClientId'

# apply to just WebSocket connections with name = ResponseTimes
Set-PodeSignalBroadcastLevel -Name 'ResponseTimes' -Type 'ClientId'

The following levels are available:

Level Description
Name A Name is required. Groups/ClientIds are optional.
Group A Name is required. One of either a Group or ClientId is required.
ClientId A Name and a ClientId are required.

Signing ClientIds

Similar to Sessions and Cookies, you can sign WebSocket connection ClientIds. This can be done by calling Enable-PodeSignalSigning and supplying a -Secret to sign the ClientIds.

Tip

You can use the inbuilt Get-PodeServerDefaultSecret function to retrieve an internal Pode server secret which can be used. However, be warned that this secret is regenerated to a random value on every server start/restart.

Enable-PodeSignalSigning -Secret 'super-secret'
Enable-PodeSignalSigning -Secret (Get-PodeServerDefaultSecret)

When signing is enabled, all clientIds will be signed regardless if they're an internally generated random GUID or supplied via -ClientId on ConvertTo-PodeSignalConnection. A signed clientId will look as follows, and have the structure s:<clientId>.<signature>:

s:5d12f974-7b1a-4524-ab93-6afbf42c4ffa.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ=

You can also supply the -Strict switch to Enable-PodeSignalSigning, which will extend the secret during signing with the client's IP Address and User Agent.

Request Headers

If you have a WebSocket connection open for a client, and you want to have the client send AJAX requests to the server but have the responses streamed back over that client's WebSocket connection, then you can identify the WebSocket connection for the client using the following HTTP headers:

  • X-PODE-SIGNAL-CLIENT-ID
  • X-PODE-SIGNAL-NAME
  • X-PODE-SIGNAL-GROUP

At a minimum, you'll need the X-PODE-SIGNAL-CLIENT-ID header. If supplied Pode will automatically verify the client ID for you, including if the signing of the client ID is valid - if you're using client ID signing.

When these headers are supplied in a request, Pode will set up the $WebEvent.Signal property again - similar to the property set up from conversion above:

Name Description
Name The Name for the connection from X-PODE-SIGNAL-NAME
Group The Group for the connection from X-PODE-SIGNAL-GROUP
ClientId The assigned ClientId for the connection from X-PODE-SIGNAL-CLIENT-ID
IsLocal $false
IsGlobal $true

Note

If you only supply the Name or Group headers, then the $WebEvent.Signal property will not be configured. The ClientId is required as a minimum.

Client Side

Receiving Messages

On the client side, you need to use javascript to register a WebSocket and then bind the onmessage event to do something when a broadcasted message is received.

To create a WebSocket, you can do something like the following which will bind a WebSocket onto the /response-times route:

$(document).ready(() => {
    // create the websocket
    var ws = new WebSocket("ws://localhost:8091/response-times");

    // event for inbound messages to append them
    ws.onmessage = function(evt) {
        var data = JSON.parse(evt.data)
        $('#messages').append(`<p>${data.Message}</p>`);
    }
})

Sending Messages

To send a message using the WebSocket, you can use the .send function. When you send a message from client-to-server, the data must be a JSON value containing the message, and optionally a path, group, clientId, direct properties.

For example, if you have a form with input, you can send the message as follows. If you have no Signal Route configured then this will broadcast to every connected client.

$('#form').submit(function(e) {
    e.preventDefault();
    ws.send(JSON.stringify({ message: $('#input').val() }));
    $('#input').val('');
})

To broadcast the message to just clients connected on a specific path, such as /receive:

$('#form').submit(function(e) {
    e.preventDefault();
    ws.send(JSON.stringify({ message: $('#input').val(), path: '/receive' }));
    $('#input').val('');
})

If you just want the server to on respond directly back to the sending client, and not broadcast to all clients, then set direct to true:

$('#form').submit(function(e) {
    e.preventDefault();
    ws.send(JSON.stringify({ message: $('#input').val(), direct: true }));
    $('#input').val('');
})

Events

Similar to Server Events there are also events which you can register scriptblocks for Signal (WebSocket) connections. Currently the following events are supported:

Event Description
Connect Triggered when an HTTP request is successfully converted to a Signal (WebSocket) connection
Disconnect Triggered when the signal connection is disconnected, either by the server or client

Register

To register a scriptblock for a Signal connection event you use Register-PodeSignalEvent. You'll need to supply the Name of the Signal connection - from ConvertTo-PodeSignalConnection, or the URI path if using auto-upgrade - which you're registering the event against, as well as the type of the event, and a name for the event registration - and of course the scriptblock itself.

For example, to register for the Connect event of a Signal connection, to write the Client ID to the CLI, you would do:

# register a Connect event
Register-PodeSignalEvent -Name 'Example' -Type Connect -EventName 'OnConnect' -ScriptBlock {
    "Connected: $($TriggeredEvent.Connection.Name) ($($TriggeredEvent.Connection.ClientId))" | Out-Default
}

# a Route to convert the HTTP request to a Signal connection
Add-PodeRoute -Method Get -Path '/signal' -ScriptBlock {
    ConvertTo-PodeSignalConnection -Name 'Example'
}

Note

For those using the legacy auto-upgrade approach, the -Name supplied to Register-PodeSignalEvent will be the URI path your WebSocket connected to. For example, if you connected to ws://localhost:8080/messages then you'll pass /messages to -Name.

Event Data

Various metadata about the Signal connection event is supplied to your scriptblock, under the $TriggeredEvent variable - including the Connection object, the same one typically found under $WebEvent.Signal:

Property Description
Lockable A global lockable value you can use for Lock-PodeObject
Metadata Any additional metadata about the event, you can add your own properties here as well
Name The Name of the Signal connection which triggered the event
Type The type of event triggered - Connect, Disconnect
Timestamp When the event was triggered, in UTC
Connection The Connection object itself, containing the connection Name, Group, ClientId, etc.

Unregister

To unregister an previous event registration, simply use Unregister-PodeSignalEvent:

# to remove the Connect event from above:
Unregister-PodeSignalEvent -Name 'Example' -Type Connect -EventName 'OnConnect'

Full Example

This full example is a tweaked version of the one found in /examples/Web-SignalManual.ps1 of the main repository.

If you open this example on multiple browsers, sending messages will be automatically received by all browsers without using async javascript!

The file structure for these files is:

server.ps1
/views
    index.html
/public
    script.js

The following is the Pode server code, that will create one route, which will be for some home page, with a button/input for broadcasting messages.

Start-PodeServer {

    # listen
    Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http
    Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -NoAutoUpgradeWebSockets

    # route for home page view
    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        Write-PodeViewResponse -Path 'websockets'
    }

    # route for websocket upgrade
    Add-PodeRoute -Method Get -Path '/msg' -ScriptBlock {
        ConvertTo-PodeSignalConnection -Name 'Msg'
    }

    # signal route, to return current date or broadcast sent message
    Add-PodeSignalRoute -Path '/msg' -ScriptBlock {
        $msg = $SignalEvent.Data.Message

        if ($msg -ieq '[date]') {
            $msg = [datetime]::Now.ToString()
        }

        Send-PodeSignal -Name 'Msg' -Value @{ message = $msg }
    }
}

Next we have the HTML web page with a basic button/input for broadcasting messages. There's also a <div> to append received messages:

<html>
    <head>
        <title>WebSockets</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
        <script type="text/javascript" src="/script.js"></script>
    </head>
    <body>
        <p>Clicking submit will broadcast the message to all connected clients</p>
        <form id='bc-form'>
            <input type='text' name='message' placeholder='Enter any random text' />
            <input type='submit' value='Broadcast!' />
        </form>

        <div id='messages'></div>
    </body>
</html>

Finally, the following is the client-side javascript to register a WebSocket for the client. It will also invoke the .send function of the WebSocket when the button is clicked:

$(document).ready(() => {
    // bind submit on the form to send message to the server
    $('#bc-form').submit(function(e) {
        e.preventDefault();

        ws.send(JSON.stringify({
            message: $('input[name=message]').val()
        }));

        $('input[name=message]').val('');
    });

    // create the websocket
    var ws = new WebSocket("ws://localhost:8091/msg");

    // event for inbound messages to append them
    ws.onmessage = function(evt) {
        $('#messages').append(`<p>${evt.data}</p>`);
    }
});