WebSocket
A WebSocket endpoint accepts any WebSocket client at:
wss://app.hooklistener.com/ws/<slug>
Every frame the client sends is one recorded message, and so is every frame Hooklistener sends back. See the overview for the console, rules, scenarios, limits and the REST API.
Connect
These are the snippets from the endpoint's Connect tab, for an endpoint with an access token. Without a token, leave out the header or subprotocol.
wscat
# npm i -g wscat
wscat -c "wss://app.hooklistener.com/ws/<slug>" -H "X-Hooklistener-Token: <token>"
> {"type":"ping"}
Browser
// Browsers can't set headers: the token travels as a subprotocol. If your app
// needs its own subprotocol, list it first: ["graphql-transport-ws", "hooklistener-token.<token>"]
const socket = new WebSocket("wss://app.hooklistener.com/ws/<slug>", ["hooklistener-token.<token>"]);
socket.onopen = () => socket.send(JSON.stringify({ type: "ping" }));
socket.onmessage = (event) => console.log("received", event.data);
socket.onclose = (event) => console.log("closed", event.code, event.reason);
Node (ws)
// npm i ws
import WebSocket from "ws";
const socket = new WebSocket("wss://app.hooklistener.com/ws/<slug>", {
headers: { "X-Hooklistener-Token": "<token>" },
});
socket.on("open", () => socket.send(JSON.stringify({ type: "ping" })));
socket.on("message", (data) => console.log("received", data.toString()));
socket.on("close", (code, reason) => console.log("closed", code, reason.toString()));
Python (websockets)
# pip install websockets
import asyncio, json, websockets
async def main():
async with websockets.connect("wss://app.hooklistener.com/ws/<slug>", additional_headers={"X-Hooklistener-Token": "<token>"}) as ws:
await ws.send(json.dumps({"type": "ping"}))
async for message in ws:
print("received", message)
asyncio.run(main())
The Connect tab also has Go (gorilla/websocket) and raw curl upgrade snippets.
Access token
Clients that can set headers send the token as X-Hooklistener-Token, as Authorization: Bearer <token>, or as ?token= in the URL. See Access token.
Browser clients
The browser WebSocket API can't set headers. Pass the token as a subprotocol named hooklistener-token.<token>:
new WebSocket(url, ["hooklistener-token.<token>"]);
If your app also needs a subprotocol of its own, list yours first: ["graphql-transport-ws", "hooklistener-token.<token>"]. Hooklistener then answers with your subprotocol. It answers with the token subprotocol only when that is the only one the client offered, because browsers fail the handshake if the server picks none. The token subprotocol is recorded only as hooklistener-token, and it is never sent to a proxy upstream.
Subprotocols
Many WebSocket protocols, such as graphql-transport-ws, STOMP or MQTT, are negotiated with the Sec-WebSocket-Protocol header. Browsers and most libraries refuse a connection when they offered subprotocols and the server picked none. Hooklistener always picks one when the client offers any:
-
Accepted subprotocols empty (the default): the server echoes the first subprotocol the client requested.
-
With a list: the server echoes the first requested subprotocol that is on the list. A client that requested none of them gets
400with a plain-text explanation, for example:None of the requested WebSocket subprotocols (graphql-ws) is accepted by this endpoint,which accepts: graphql-transport-ws. Request one of those, or change the endpoint'ssubprotocol settings.Turn on When none on the list match, accept the client's first choice instead of refusing to accept such clients anyway.
-
A client that requests no subprotocol gets none.
These settings are under Settings → WebSocket handshake.
Frames
- Text and binary frames are both recorded with their frame type.
- Messages you send are text frames. A payload that is not valid UTF-8 goes out as a binary frame, because clients close the connection when a text frame isn't UTF-8. The console's Binary (b64) mode and the API's
"opcode": "binary"send a base64 payload as a binary frame. - An Echo rule sends back the same bytes in the same frame type.
- Turn on Record ping/pong control frames to record the client's pings and pongs, and pings you send from the console. Hooklistener's own keepalive pings are never recorded.
- A message can be up to 1 MB, including a fragmented message as a whole. A bigger one closes the connection with
1009andmessage too large: <n> bytes (limit 1 MB).
No compression
Hooklistener does not negotiate permessage-deflate. Frames go over the wire exactly as recorded. Compression caused two problems:
- An empty compressed message could make clients fail to inflate the next frame and drop the connection.
- Highly compressible messages were refused with
1009long before the size limit.
Your client needs no change: it simply doesn't compress.
Keepalive
The server pings every 25 seconds. A client that sends nothing for 75 seconds, not even a pong, is closed with 1001 and keepalive timeout: nothing received from the client for 75s. Browsers and WebSocket libraries answer pings automatically. You only need to act if your client disables pong replies.
Close codes
When the client closes, its close code and reason are recorded on the session, for example 4001 bye. A connection that drops without a close frame is recorded as 1006. When you close a session from the console or the API, the client receives the code and reason you chose. See Close codes.
Proxy mode
Set the Upstream URL to your real server (wss://api.example.com/socket). Every client connection then opens its own upstream connection:
- Byte for byte. Frames are relayed unchanged in both directions, text and binary, and each one is recorded. Templating never touches relayed frames.
- Early frames are kept. Frames the client sends before the upstream handshake completes are queued and sent in order once it does (up to 8 MB). Protocols that send on open, like
graphql-ws'sconnection_init, work. - Your credentials stay with Hooklistener. The
hooklistener-token.*subprotocol is never forwarded, and neither isX-Hooklistener-Token.?token=,?access_token=andAuthorization: Bearerare dropped when they carry the endpoint's access token. Your app's own tokens pass through. - Forwarded to the upstream: the client's query string, and, with Forward the client's negotiated subprotocol, user-agent and Authorization headers on, the negotiated subprotocol and those headers. Extra upstream headers are always added.
- Close codes go both ways. The upstream's close code and reason reach the client unchanged, and the client's reach the upstream. If the upstream can't be reached or fails, the client is closed with
1011andupstream error: …. - The upstream gets a ping every 25 seconds too, so an idle proxied connection stays open.
Rules, expectations and contract validation still run on what the client sends. Use → upstream in the console composer to send a frame to your server as if the client had sent it.