Server-Sent Events (SSE)
Server-Sent Events (SSE) enables real-time streaming from HTTP ingresses to clients. When a client requests an SSE stream, the ingress keeps the connection open and pushes events as they occur.
Enabling SSE
SSE mode is automatically enabled when a client sends the Accept: text/event-stream header:
curl -N -H "Accept: text/event-stream" \ https://example.hantera.io/ingress/api/orders/ORD-123/eventsThe same HTTP ingress can serve both regular JSON responses and SSE streams based on the Accept header.
Message Format
SSE messages are sent as records with special fields that map to SSE protocol fields:
from { event = 'orderUpdated' data = { orderId = 'ORD-123', status = 'shipped' }}Output:
event: orderUpdateddata: {"orderId":"ORD-123","status":"shipped"}Standard SSE Fields
| Field | Purpose | Serialization |
|---|---|---|
event | Event type for addEventListener() | String |
data | Main payload | JSON serialized |
id | Event ID for reconnection tracking | String |
retry | Reconnection delay hint (ms) | String |
All other fields are serialized as strings and passed through to the client.
Example with All Fields
from { event = 'heartbeat' id = 'hb-42' retry = 5000 data = { timestamp = now }}Output:
event: heartbeatid: hb-42retry: 5000data: {"timestamp":"2026-01-11T19:30:00Z"}Simple Values
Non-record values are automatically wrapped in data::
from 'Hello world'Output:
data: "Hello world"Real-Time Events with events()
The events() function subscribes to Hantera’s event bus, enabling real-time updates:
param orderId: text
from events ($'actors/order/{orderId}', 'checkpoint') select e => { event = 'updated', data = e }This subscribes to checkpoint events for a specific order actor and streams them to the client.
Complete Example: SKU Stock Updates
Here’s a real-world example streaming real-time stock availability for a SKU:
Ingress Configuration
uri: /resources/ingresses/api/skus/stock-eventsspec: type: http componentId: sku-stock-events.hreactor acl: - skus:read properties: route: api/skus/{skuNumber}/stock httpMethod: getComponent Code
import 'text'
param skuNumber: text
let safeSkuNumber = skuNumber replace("''", "''''")let skuQuery = query skus(skuId, skuNumber) filter $'skuNumber == ''{safeSkuNumber}'''
from skuQuery match (e: QueryError) |> { error = { code = 'QUERY_ERROR', message = e.message } } |> let sku = skuQuery first
from sku match nothing |> { error = { code = 'NOT_FOUND', message = 'SKU not found' } } |> let initialStock = messageActor( 'sku' sku.skuId [{ type = 'calculateAvailableStock' }] )
from { event = 'init', data = initialStock }
from events ($'actors/sku/{sku.skuId}', 'checkpoint') select e => let stock = messageActor( 'sku' sku.skuId [{ type = 'calculateAvailableStock' }] ) from { event = 'stockUpdated', data = stock }This ingress:
- Looks up the SKU by its skuNumber
- Sends an
initevent with the current available stock - Streams
stockUpdatedevents whenever the SKU’s stock changes
Client-Side Consumption
const skuNumber = 'PROD-001'const eventSource = new EventSource( `https://api.example.com/ingress/api/skus/${skuNumber}/stock`)
eventSource.addEventListener('init', (e) => { const stock = JSON.parse(e.data) console.log('Initial stock:', stock) updateStockDisplay(stock)})
eventSource.addEventListener('stockUpdated', (e) => { const stock = JSON.parse(e.data) console.log('Stock updated:', stock) updateStockDisplay(stock)})
eventSource.onerror = (e) => { console.log('Connection error, auto-reconnecting...')}
// Clean up when donefunction cleanup() { eventSource.close()}Multiple Streams
You can combine multiple event sources and immediate values:
param orderId: uuidparam paymentId: uuid
from { event = 'init', data = { orderId, paymentId } }
from events ($'actors/order/{orderId}', 'checkpoint') select e => { event = 'orderUpdated', data = e }
from events ($'actors/payment/{paymentId}', 'checkpoint') select e => { event = 'paymentUpdated', data = e }Multiple iterators are merged and events are delivered as they arrive from any source.
Error Handling
Pre-Stream Errors
Errors returned before streaming starts result in standard HTTP errors:
import 'text'
param skuNumber: text
let safeSkuNumber = skuNumber replace("''", "''''")let skuQuery = query skus(skuId) filter $'skuNumber == ''{safeSkuNumber}'''
from skuQuery match (e: QueryError) |> { error = { code = 'QUERY_ERROR', message = e.message } } |> let sku = skuQuery first from sku match nothing |> { error = { code = 'NOT_FOUND', message = 'SKU not found' } } |> { event = 'init', data = sku }If the SKU doesn’t exist, the client receives:
HTTP/1.1 404 Not FoundContent-Type: application/json
{"error":{"code":"NOT_FOUND","message":"SKU not found"}}The EventSource.onerror handler will fire for HTTP error responses.
Application-Level Errors
For errors during streaming, send an error event:
from events (topic, eventType) select e => e match { error: text } |> { event = 'error', data = { code = 'STREAM_ERROR', message = e.error } } |> { event = 'updated', data = e }Client handling:
eventSource.addEventListener('error', (e) => { const error = JSON.parse(e.data) console.error('Application error:', error)})Connection Errors
Connection drops trigger the onerror handler and automatic reconnection:
eventSource.onerror = (e) => { if (eventSource.readyState === EventSource.CONNECTING) { console.log('Reconnecting...') } else if (eventSource.readyState === EventSource.CLOSED) { console.log('Connection closed') }}Keepalive
Hantera sends keepalive comments every 30 seconds to maintain the connection:
:keepaliveThese are automatically ignored by EventSource clients but prevent proxy timeouts.
Reconnection Behavior
The browser’s EventSource API automatically reconnects on connection loss:
- Connection drops
- Browser waits (default 3000ms, or value from
retryfield) - Browser reconnects with
Last-Event-IDheader if IDs were sent
On reconnection, the component is re-executed. Design your component to handle this by always sending initial state:
from { event = 'init', data = currentState }
from events (topic, eventType) select e => { event = 'updated', data = e }Testing with cURL
curl -N -H "Accept: text/event-stream" \ https://example.hantera.io/ingress/api/skus/PROD-001/stockThe -N flag disables buffering for immediate output.
See Also
- HTTP Ingresses - Overview and configuration
- Parameter Mapping - Headers, query params, and body handling
- Response Handling - JSON, streams, and error responses