Usage

Subscribing

When clients make HTTP requests or WebSocket connections to Pushpin, what happens next depends on the instructions provided by your backend web service. These instructions can include subscribing the client to one or more channels.

For HTTP-based transports, this is typically done with response headers. For example:

Grip-Hold: stream
Grip-Channel: mychannel

For the WebSocket transport, this is done using control messages sent by the backend to Pushpin. For example:

c:{"type": "subscribe", "channel": "mychannel"}

It’s important to understand that clients don’t assert their own subscriptions. Clients make arbitrary HTTP requests or send arbitrary WebSocket messages, and it is your backend that determines whether or not clients should be subscribed to anything. Your channel schema remains private between Pushpin and your backend server, and in fact clients may not even be aware that publish-subscribe activities are occurring.

See the Transports section below for transport-specific subscribing details.

Publishing

Publishing Overview

Data is published to clients by making a request to Pushpin’s HTTP control API or input ZeroMQ sockets. There are four possible inputs:

Messages are formatted as “items” in either JSON or tnetstring format, depending on the input used. Here’s an example of an item:

{
  "channel": "mychannel",
  "id": "an-item-id",
  "formats": {
    "http-stream": {
      "content": "a chunk of data\n"
    }
  }
}

With the HTTP control API, the item is enveloped to allow sending multiple items at once:

POST /publish HTTP/1.1
Host: localhost:5561
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "id": "an-item-id",
      "formats": {
        "http-stream": {
          "content": "a chunk of data\n"
        }
      }
    }
  ]
}

With the ZeroMQ input PULL socket, each message is single-part containing one item in tnetstring format or JSON format, prefixed with a single character (T or J) indicating the format type.

Example tnetstring message:

T104:7:formats,49:11:http-stream,30:7:content,16:a chunk of data
,}}7:channel,9:mychannel,2:id,10:an-item-id,}

Note that there’s a literal newline in the above payload, after the word “data”.

Example JSON message:

J{"id": "an-item-id", "channel": "mychannel", "formats": {"http-stream": {"content": "a chunk of data\n"}}}

With the ZeroMQ input SUB socket, each message is multi-part, where the first part is the channel, and the second part is an item payload in one of the above formats. The channel field is ignored in the item payload.

One or more transport formats must be provided for each item. Messages are only delivered to connections if there is a format for the connection type. The defined formats are http-stream, http-response, and ws-message.

Clients never publish data to other clients. Only the backend server can publish data.

See the Transports section for transport-specific publishing details.

Publish actions

Transport formats can specify one of several actions:

The hint action is useful if it is difficult for the publisher to generate the needed content, or if you need to publish a large payload. This only works for reliable connections, and the effect is similar to the Recover command except it is processed immediately rather than soon-ish.

Example published hint:

POST /publish HTTP/1.1
Host: localhost:5561
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "id": "an-item-id",
      "prev-id": "another-item-id",
      "formats": {
        "http-stream": {
          "action": "hint"
        }
      }
    }
  ]
}

The close action tells Pushpin to end/close the HTTP request or WebSocket connection with the subscriber. This works for HTTP streaming and WebSockets. It doesn’t work for HTTP long-polling, since publishing data ends the request anyway and it’s better to send something than nothing.

Example published close:

POST /publish HTTP/1.1
Host: localhost:5561
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "id": "an-item-id",
      "formats": {
        "http-stream": {
          "action": "close"
        }
      }
    }
  ]
}

For the ws-message format, you can provide a close code and reason:

POST /publish HTTP/1.1
Host: localhost:5561
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "id": "an-item-id",
      "formats": {
        "ws-message": {
          "action": "close",
          "code": 1013,
          "reason": "bye for now",
        }
      }
    }
  ]
}

Binary data

Pushpin supports publishing binary data to subscribers.

Internally, Pushpin treats all content as bytes, and if you publish data to the ZeroMQ PULL or SUB sockets using tnetstring format, then the payload is already binary. The issue of sending binary data only arises when JSON format is used, such as with the HTTP publish command.

To send binary data within a JSON-formatted item, the data must be Base64-encoded and set to an appropriate -bin-suffixed field. For http-stream and ws-message items, set content-bin with Base64 data and omit content. For http-response, set body-bin with Base64 data and omit body.

If you are using a library for publishing, it is possible the library takes care of this for you. For example, the Python gripcontrol library automatically does the right thing with HTTP data, and for WebSockets all you need to do is set binary=True on the format object.

Transports

HTTP streaming

When Pushpin receives an HTTP request from a client, Pushpin forwards it as-is to the origin server that you specified in the routes file. The origin server then tells Pushpin either to respond immediately with a normal HTTP response or to hold the HTTP connection open instead. If a connection is held open, then it can be used later on for data pushing.

You can service incoming HTTP requests however you want. Normal responses sent to Pushpin will be relayed to clients as-is. To cause a request to be held open and subscribed to channels you need to include special instructions via headers. Include a Grip-Hold header to indicate that a request should be held open. For streaming, its value should be stream (for long-polling, see HTTP long-polling).

For example, here’s how the origin server should respond to hold a request open as a stream, subscribed to mychannel:

HTTP/1.1 200 OK
Content-Type: text/plain
Grip-Hold: stream
Grip-Channel: mychannel

Stream opened, prepare yourself

When Pushpin receives this response from your origin server, it will process it as an instruction. At this point, the HTTP request/response interaction between Pushpin and the origin server is complete, but the HTTP request/response interaction between the client and Pushpin remains.

Constructing a hold response is easy in your favorite language:

if request.method == 'GET':
    response = HttpResponse(
        'Stream opened, prepare yourself!\n',
        content_type='text/plain')
    response['Grip-Hold'] = 'stream'
    response['Grip-Channel'] = 'mychannel'
    return response
def do_GET(request, response):
    response.status = 200
    response['Content-Type'] = 'text/plain'
    response['Grip-Hold'] = 'stream'
    response['Grip-Channel'] = 'mychannel'
    response.body = "Stream opened, prepare yourself!\n"
<?php

header('Content-Type: text/plain');
header('Grip-Hold: stream');
header('Grip-Channel: mychannel');

?>
Stream opened, prepare yourself!
http.createServer(function (req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/plain',
        'Grip-Hold': 'stream',
        'Grip-Channel': 'mychannel'
    });
    res.end('Stream opened, prepare yourself!\n');
}).listen(80, '0.0.0.0');
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py:

# pip install django-grip
from django_grip import set_hold_stream

if request.method == 'GET':
    set_hold_stream(request, 'mychannel')
    return HttpResponse('Stream opened, prepare yourself!\n',
        content_type='application/json')

When a client makes a request through Pushpin and your origin server responds with a hold instruction in stream mode as shown above, then the client will immediately receive an initial response:

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

Stream opened, prepare yourself!

Note that the Grip-* headers were stripped. Also, the request won’t have Content-Length set, so that it can stay open indefinitely as you publish additional body content.

Whenever you want to send data to listening clients, publish an “HTTP stream” payload containing the content:

# pip install gripcontrol
from gripcontrol import GripPubControl

pub = GripPubControl({
    'control_uri': 'http://localhost:5561'
})

pub.publish_http_stream('mychannel', 'hello there\n')
# gem install gripcontrol
require 'gripcontrol'

pub = GripPubControl.new({
    'control_uri' => 'http://localhost:5561'
})

pub.publish_http_stream_async('mychannel', "hello there\n")
<?php
// composer require fanout/gripcontrol

$pub = new GripPubControl(array(
    'control_uri' => 'http://localhost:5561'
));

$pub->publish_http_stream('mychannel', 'hello there\n');

?>
// npm install --save grip
var grip = require('grip');

var pub = new grip.GripPubControl({
    'control_uri': 'http://localhost:5561'
});

pub.publishHttpStream('mychannel', 'hello there\n');
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py or elsewhere:

# pip install django-grip
from gripcontrol import HttpStreamFormat
from django_grip import publish

publish('mychannel', HttpStreamFormat('hello there\n'))

If there’s no library for your programming language, publishing is just one HTTP POST:

POST /publish/ HTTP/1.1
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "formats": {
        "http-stream": {
          "content": "hello there\n"
        }
      }
    }
  ]
}

Stream holds remain open and can be repeatedly pushed to. Content you send may be of any type. In the above examples, we are publishing plain text lines instead of JSON.

You can also stream data using Server-sent events.

Server-sent events

Pushpin makes it easy to implement Server-Sent Events. Simply use HTTP streaming, with the following guidelines for holding and publishing.

On the origin server, respond with hold instructions using stream mode and content type text/event-stream:

if request.method == 'GET':
    response = HttpResponse(content_type='text/event-stream')
    response['Grip-Hold'] = 'stream'
    response['Grip-Channel'] = 'mychannel'
    return response
def do_GET(request, response):
    response.status = 200
    response['Content-Type'] = 'text/event-stream'
    response['Grip-Hold'] = 'stream'
    response['Grip-Channel'] = 'mychannel'
<?php

header('Content-Type: text/event-stream');
header('Grip-Hold: stream');
header('Grip-Channel: mychannel');

?>
http.createServer(function (req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Grip-Hold': 'stream',
        'Grip-Channel': 'mychannel'
    });
    res.end();
}).listen(80, '0.0.0.0');
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py:

# pip install django-grip
from django_grip import set_hold_stream

if request.method == 'GET':
    set_hold_stream(request, 'mychannel')
    return HttpResponse(content_type='text/event-stream')

Then, whenever you want to send data, publish an HTTP stream payload containing SSE formatting:

# pip install gripcontrol
from gripcontrol import GripPubControl

pub = GripPubControl({
    'control_uri': 'http://localhost:5561'
})

pub.publish_http_stream('mychannel',
    'event: update\ndata: hello world\n\n')
# gem install gripcontrol
require 'gripcontrol'

pub = GripPubControl.new({
    'control_uri' => 'http://localhost:5561'
})

pub.publish_http_stream_async('mychannel',
    "event: update\ndata: hello world\n\n")
<?php
// composer require fanout/gripcontrol

$pub = new GripPubControl(array(
    'control_uri' => 'http://localhost:5561'
));

$pub->publish_http_stream('mychannel',
    'event: update\ndata: hello world\n\n');

?>
// npm install --save grip
var pub = new grip.GripPubControl({
    'control_uri': 'http://localhost:5561'
});

pub.publishHttpStream('mychannel',
    'event: update\ndata: hello world\n\n');
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py or elsewhere:

# pip install django-grip
from gripcontrol import HttpStreamFormat
from django_grip import publish

publish('mychannel', HttpStreamFormat(
    'event: update\ndata: hello world\n\n'))

HTTP long-polling

When Pushpin receives an HTTP request from a client, Pushpin forwards it as-is to the origin server that you specified in the routes file. The origin server then tells Pushpin either to respond immediately with a normal HTTP response or to hold the HTTP connection open instead. If a connection is held open, then it can be used later on for data pushing. When data is to be pushed over a held connection, the complete HTTP response is specified, headers and all.

You can service incoming HTTP requests however you want. Normal responses sent to Pushpin will be relayed to clients as-is. To cause a request to be held open and subscribed to channels you need to include special instructions via headers. Include a Grip-Hold header to indicate that a request should be held open. For long-polling, its value should be response (for streaming, see HTTP streaming).

For example, here’s how the origin server should respond to hold a request open as a long-poll, subscribed to mychannel:

HTTP/1.1 200 OK
Content-Type: application/json
Grip-Hold: response
Grip-Channel: mychannel

{}

When Pushpin receives this response from your origin server, it will process it as an instruction rather than relaying it to the client. At this point, the HTTP request/response interaction between Pushpin and the origin server is complete, but the HTTP request/response interaction between the client and Pushpin remains.

After 55 seconds pass (you can override this with Grip-Timeout), the request will time out and Pushpin will send the above response to the client with the Grip-* headers stripped. Here we are basically saying that if there is no data to publish on this channel before the request times out, then the client should receive an empty JSON object.

For example, if the request times out, then the client would see something like this:

HTTP/1.1 200 OK
Content-Type: application/json

{}

Of course, you may specify any content as the timeout response. You can use any headers and the body doesn’t have to be JSON.

Constructing a hold response is easy in your favorite language:

if request.method == 'GET':
    response = HttpResponse('{}\n',
        content_type='application/json')
    response['Grip-Hold'] = 'response'
    response['Grip-Channel'] = 'mychannel'
    return response
def do_GET(request, response):
    response.status = 200
    response.body = "{}\n"
    response['Content-Type'] = 'application/json'
    response['Grip-Hold'] = 'response'
    response['Grip-Channel'] = 'mychannel'
<?php

header('Content-Type: application/json');
header('Grip-Hold: response');
header('Grip-Channel: mychannel');

?>
{}
http.createServer(function (req, res) {
    res.writeHead(200, {
        'Content-Type': 'application/json',
        'Grip-Hold': 'response',
        'Grip-Channel': 'mychannel'
    });
    res.end('{}\n');
}).listen(80, '0.0.0.0');
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py:

# pip install django-grip
from django_grip import set_hold_longpoll

if request.method == 'GET':
    set_hold_longpoll(request, 'mychannel')
    return HttpResponse('{}\n',
        content_type='application/json')

Whenever you want to send data to listening clients, publish an “HTTP response” payload containing the content:

# pip install gripcontrol
from gripcontrol import GripPubControl

pub = GripPubControl({
    'control_uri': 'http://localhost:5561'
})

pub.publish_http_response('mychannel', '{"hello": "world"}\n')
# gem install gripcontrol
require 'gripcontrol'

pub = GripPubControl.new({
    'control_uri' => 'http://localhost:5561'
})

pub.publish_http_response_async('mychannel',
    "{\"hello": "world\"}\n")
<?php
// composer require fanout/gripcontrol

$pub = new GripPubControl(array(
    'control_uri' => 'http://localhost:5561'
));

$pub->publish_http_response('mychannel',
    '{"hello": "world"}\n');

?>
// npm install --save grip
var grip = require('grip');

var pub = new grip.GripPubControl({
    'control_uri': 'http://localhost:5561'
});

pub.publishHttpResponse('mychannel', '{"hello": "world"}\n');
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py or elsewhere:

# pip install django-grip
from gripcontrol import HttpResponseFormat
from django_grip import publish

publish('mychannel',
    HttpResponseFormat('{"hello": "world"}\n'))

If there’s no library for your programming language, publishing is just one HTTP POST:

POST /publish/ HTTP/1.1
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "formats": {
        "http-response": {
          "body": "{\"hello\": \"world\"}\n"
        }
      }
    }
  ]
}

In the above publishing examples, we are specifying the HTTP response body to send to listening clients. The headers in the original instructional response will be merged with this content, minus any Grip-* headers. After publishing data, clients with held requests would receive a response looking like this:

HTTP/1.1 200 OK
Content-Type: application/json

{"hello": "world"}

For consuming a long-polling API from the browser, we recommend the Pollymer AJAX library (not to be confused with Polymer).

WebSockets

When an incoming WebSocket connection request is received, Pushpin opens a WebSocket connection (or uses an emulation over HTTP, see below) to the origin server that you specified in the routes file. Pushpin does not accept the incoming WebSocket connection from the client until its outbound WebSocket connection has been accepted by the origin. Once the connections are established, messages can flow end-to-end in either direction. Using special control messages, you can subscribe connections to channels and then publish WebSocket messages to be injected into those connections.

Pushpin communicates with the origin server in one of two modes:

  1. Normal WebSockets: For every WebSocket connection between a client and Pushpin, there will be a corresponding WebSocket connection between Pushpin and the origin server.

  2. WebSocket-over-HTTP protocol: Instead of using a WebSocket connection to the origin, Pushpin encodes WebSocket events into HTTP requests/responses. Events include OPEN, TEXT, PING, PONG, and CLOSE, and are encoded in a format similar to HTTP chunked encoding. You don’t really have to think about the encoding, though, as our server libraries take care of it for you. For details, see the WebSocket-over-HTTP specification.

We recommend using the WebSocket-over-HTTP protocol unless you have a good reason not to use it. The approach is great for stateless application protocols, and the origin server doesn’t have to maintain long-lived connections nor even support WebSockets. The one gotcha is that the origin server can only send spontaneous messages by publishing, but in a stateless protocol this should be expected. The WebSocket-over-HTTP mode is enabled by setting the over_http option on the target in the routes file.

Having Pushpin proxy your WebSocket connections alone is not very interesting. In order to use publish-subscribe, the grip WebSocket extension must be negotiated. Pushpin will include a Sec-WebSocket-Extensions request header with this extension, which must be acknowledged in the origin response. You need to do this regardless of whether Pushpin is communicating with your server using a normal WebSocket or with the WebSocket-over-HTTP protocol.

For example, a WebSocket connection request might look like this:

GET /path HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=
Sec-WebSocket-Extensions: grip

In which case the origin server should respond as such:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Extensions: grip

If Pushpin is communicating to the origin server using the WebSocket-over-HTTP protocol, then a connection request might look like the example below. Note that the characters \r\n represent a two-byte sequence of carriage return and newline.

POST /path HTTP/1.1
Sec-WebSocket-Extensions: grip
Content-Type: application/websocket-events
Accept: application/websocket-events

OPEN\r\n

The origin server should then respond accordingly. Note that even though this is not a WebSocket connection, the Sec-WebSocket-Extensions header is still used to negotiate GRIP:

HTTP/1.1 200 OK
Sec-WebSocket-Extensions: grip
Content-Type: application/websocket-events

OPEN\r\n

At this point, the proxied WebSocket session is established with GRIP activated. If Pushpin has a normal WebSocket connection with the origin server, then Pushpin and the origin server may exchange WebSocket messages normally. If Pushpin has a WebSocket-over-HTTP session with the origin server, then Pushpin and the origin server may exchange WebSocket “events” over HTTP.

When a WebSocket session has GRIP mode activated, messages sent from the origin server to Pushpin must be prefixed with either m: (regular message) or c: (control message). A message without a prefix will be ignored by Pushpin. The prefixing is needed to disambiguate regular messages from control messages. When Pushpin receives a regular message in this way, the prefix is stripped before relaying the message to the client.

It is possible to override the regular message prefix by specifying the message-prefix parameter in the GRIP extension negotiation response. You can even set it to a blank string, allowing you to send normal messages without a prefix, if you’re sure that none of your regular messages will ever begin with c:. For example:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Extensions: grip; message-prefix=""

Control messages are formatted as a JSON object following the c: prefix. The object has a type field that indicates the type of control message. All other fields of the object depend on the type.

There are three possible control messages:

Here’s how the origin server would subscribe a connection to a channel called “mychannel”:

c:{"type": "subscribe", "channel": "mychannel"}

If you are using WebSocket-over-HTTP communication, then the message must be wrapped in a TEXT event and sent in an HTTP response:

HTTP/1.1 200 OK
Content-Type: application/websocket-events

TEXT 2F\r\n
c:{"type": "subscribe", "channel": "mychannel"}\r\n

Accepting a WebSocket-over-HTTP connection and subscribing it to a channel is easy in your favorite language. Our server libraries take care of encoding events for you:

# pip install gripcontrol
from gripcontrol import WebSocketEvent, \
    decode_websocket_events, encode_websocket_events, \
    websocket_control_message

if request.method == 'POST':
    in_events = decode_websocket_events(request.body)
    out_events = []
    if in_events[0].type == 'OPEN':
        out_events.append(WebSocketEvent('OPEN'))
        out_events.append(WebSocketEvent('TEXT', 'c:' +
            websocket_control_message('subscribe',
            {'channel': 'mychannel'})))
    resp = HttpResponse(encode_websocket_events(out_events),
        content_type='application/websocket-events')
    resp['Sec-WebSocket-Extensions'] = 'grip'
    return resp
# gem install gripcontrol
require 'gripcontrol'

def do_POST(request, response):
    response.status = 200
    response['Sec-WebSocket-Extensions'] = 'grip'
    response['Content-Type'] = 'application/websocket-events'

    in_events = GripControl.decode_websocket_events(
        request.body)
    out_events = []
    if in_events[0].type == 'OPEN'
        out_events.push(WebSocketEvent.new('OPEN'))
        out_events.push(WebSocketEvent.new('TEXT', 'c:' +
            GripControl.websocket_control_message('subscribe',
            {'channel' => 'mychannel'})))
    response.body = GripControl.encode_websocket_events(
        out_events)
<?php
// composer require fanout/gripcontrol

$in_events = GripControl::decode_websocket_events(
    file_get_contents("php://input"));
$out_events = array();
if ($in_events[0]->type == 'OPEN')
{
    $out_events[] = new WebSocketEvent('OPEN');
    $out_events[] = new WebSocketEvent('TEXT', 'c:' .
        GripControl::websocket_control_message('subscribe',
        array('channel' => 'mychannel')));
}

header('Content-Type: application/websocket-events');
header('Sec-WebSocket-Extensions: grip');
http_response_code(200);

echo GripControl::encode_websocket_events($out_events);

?>
// npm install --save grip
var grip = require('grip');

http.createServer(function (req, res) {
    res.writeHead(200, {
        'Sec-WebSocket-Extensions': 'grip',
        'Content-Type': 'application/websocket-events'
    });

    var body = '';
    req.on('data', function (chunk) {
        body += chunk;
    });

    req.on('end', function() {
        var inEvents = grip.decodeWebSocketEvents(body);
        var outEvents = [];
        if (inEvents[0].getType() == 'OPEN') {
            outEvents.push(new grip.WebSocketEvent('OPEN'));
            outEvents.push(new grip.WebSocketEvent('TEXT',
                'c:' + grip.webSocketControlMessage(
                'subscribe',
                {'channel': 'mychannel'})));
        }

        res.end(grip.encodeWebSocketEvents(outEvents));
    });
}).listen(80, '0.0.0.0');
# pip install django-grip

# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py:

def endpoint(request):
    # middleware provides a pseudo-socket object
    ws = request.wscontext

    # if this is a new connection, accept it and subscribe it
    # to a channel
    if ws.is_opening():
        ws.accept()
        ws.subscribe('mychannel')

    # here we loop over any messages
    while ws.can_recv():
        message = ws.recv()

        # if return value is None, then the connection is
        # closed
        if message is None:
            # ack the close
            ws.close()
            break

    # return an empty response, which middleware will fill
    # with events
    return HttpResponse()

Whenever you want to send data to listening clients, publish a “WebSocket message” payload containing the content:

# pip install gripcontrol
from pubcontrol import Item
from gripcontrol import GripPubControl, WebSocketMessageFormat

pub = GripPubControl({
    'control_uri': 'http://localhost:5561'
})

item = Item(WebSocketMessageFormat('{"hello": "world"}'))
pub.publish('mychannel', item)
# gem install gripcontrol
require 'pubcontrol'
require 'gripcontrol'

pub = GripPubControl.new({
    'control_uri' => 'http://localhost:5561'
})

item = Item.new(
    WebSocketMessageFormat.new('{"hello": "world"}'))
pub.publish_async('mychannel', item)
<?php
// composer require fanout/gripcontrol

$pub = new GripPubControl(array(
    'control_uri' => 'http://localhost:5561'
));

$item = new Item(
    new WebSocketMessageFormat('{"hello": "world"}'));
$pub->publish('mychannel', $item);

?>
// npm install --save grip
var pubcontrol = require('pubcontrol');
var grip = require('grip');

var pub = new grip.GripPubControl({
    'control_uri': 'http://localhost:5561'
});

var item = new pubcontrol.Item(
    new grip.WebSocketMessageFormat('{"hello": "world"}'));
pub.publish('mychannel', item);
# settings.py:

GRIP_PROXIES = [
    {
        'control_uri': 'http://localhost:5561'
    }
]

MIDDLEWARE_CLASSES = (
    'django_grip.GripMiddleware',
    ...
)

# views.py or elsewhere:

# pip install django-grip
from gripcontrol import WebSocketMessageFormat
from django_grip import publish

publish('mychannel',
    WebSocketMessageFormat('{"hello": "world"}'))

If there’s no library for your programming language, publishing is just one HTTP POST:

POST /publish/ HTTP/1.1
Content-Type: application/json

{
  "items": [
    {
      "channel": "mychannel",
      "formats": {
        "ws-message": {
          "content": "{\"hello\": \"world\"}\n"
        }
      }
    }
  ]
}

Libraries

There are a number of convenience libraries available for working with Pushpin on the server side:

If you’re wondering where the client side libraries are, there aren’t any, because Pushpin has no client protocol! Any client libraries would be your own to make.

Browser features

Auto cross-origin

Many API developers don’t care about cross-origin restrictions. They build an API at api.example.com, and they just want it to be usable by anyone from anywhere.

To help with this, Pushpin has a feature called Auto Cross-Origin. If enabled, Pushpin automatically provides CORS and JSONP wrapping on behalf of any backend servers.

Enabling:

To enable Auto Cross-Origin for all routes, set auto_cross_origin=true in the proxy section of pushpin.conf. To enable Auto Cross-Origin on individual routes, include the aco parameter in the route condition.

Congrats! Your web service now works from everywhere and you can get on with your life.

Why inside the proxy?

Even though it is possible for backend applications to handle CORS and JSONP on their own, and perhaps quite easily with the right web framework middleware, Pushpin’s out-of-band push mechanism adds additional complexity here. CORS headers and JSONP framing need to be included in the POST publish commands sent to Pushpin, otherwise you end up with your non-realtime interactions succeeding but realtime interactions failing.

Having to include CORS information in published payloads is not so bad, as CORS is the modern way of doing things and developers should be quite familiar with it. However, we think it’s terrible that anyone should have to write code related to JSONP in the present day. So, the main motivation of the Auto Cross-Origin feature is to relieve developers from having to maintain legacy JSONP code. That it also handles CORS is a nice bonus.

CORS:

When the Auto Cross-Origin feature is enabled, Access-Control-* headers are automatically provided:

JSONP:

If a GET request is received with a callback query parameter, then Pushpin will proxy the request out as a POST instead. The method can be overriden with the _method query parameter. Headers can be provided with the _headers query parameter containing a JSON object of header keys and values. The request body can be provided with the _body query parameter, containing arbitrary data. It is possible to fine-tune how JSONP works using Pushpin’s various jsonp_* condition parameters in the routes file.

There are two JSONP response modes: basic and extended. The default is extended, where the entire HTTP response is encoded in a JSON value passed to the callback:

HTTP/1.1 200 OK
Content-Type: text/javascript

callback({
    "code": 200,
    "reason": "OK",
    "headers": {
        "Content-Type": "text/plain"
    },
    "body": "{\"foo\": \"bar\"}"
});

In basic mode, only the response body is provided:

HTTP/1.1 200 OK
Content-Type: text/javascript

callback({"foo": "bar"});

Notably, in basic mode you lose access to the response code and headers. Also, the body must be JSON, whereas in extended mode the body can be arbitrary data.

SockJS

Pushpin is able to translate from the SockJS protocol. This allows you to handle SockJS clients using pure WebSocket backend code. Enable it using the sockjs route condition parameter.

For example, the following route supports the SockJS protocol on path /sockjs and forwards it as a WebSocket connection to the backend using path /.

*,sockjs=/sockjs,sockjs_as_path=/ target:8000

This feature also works with the over_http target parameter, so that an HTTP backend can maintain SockJS connections.