sondahub / Socket.IO test server
Socket.IO test server
A free public Socket.IO server for v3 and v4 clients: a namespace per mock API with every call over acknowledgements and the API's live activity pushed as events, plus echo, binary, server-side acks, an auth-protected namespace and disconnects on demand. Writes are validated like a real server's and are there when you read them back. No key.
WebSocket transport only — set transports: ['websocket'] — and every connection gets its own world. Both explained below.
Connect
- URL
- https://api.sondahub.com
- Path
- /socket.io/ (the default)
- Transport
- websocket — set transports: ['websocket']
- Protocol
- Socket.IO 5 over Engine.IO 4 (clients 3.x and 4.x)
- Auth
- none, except /admin: auth: { token: 'sonda-probe-key' }
| Namespace | What it does |
|---|---|
/ | The test events: echo, time, sleep, fail, ask, clock, binary, kick, drop. |
/admin | The same, behind auth: { token } — for trying connect_error. |
/store | Store: 45 calls over acks, and live orders, inventory |
/fleet | Fleet: 25 calls over acks, and live telemetry, alerts, devices |
/bank | Bank: 30 calls over acks, and live fx, transactions |
/social | Social: 25 calls over acks, and live posts, likes |
/helpdesk | Helpdesk: 25 calls over acks, and live tickets, messages |
/flights | Flights: 20 calls over acks, and live board, bookings |
/identity | Identity: 15 calls over acks, and live directory, logins |
How it answers
Every request is answered with one object: { ok: true, data }, or { ok: false, error: { code, message } } — echo alone answers exactly what it was sent. Emit with an acknowledgement and the answer comes to it, so emitWithAck resolves with the object; emit without one and it comes back as an event of the same name. Error codes: invalid_params (with details, one per refused field), not_found, conflict, precondition, rate_limited, session_full, unknown_event, server.
JavaScript (socket.io-client 4)
import { io } from 'socket.io-client'
let session = null // the token of your writes, kept from the "session" event
const store = io('https://api.sondahub.com/store', { transports: ['websocket'] })
store.on('connect', () => console.log('connected as', store.id))
store.on('orders', (order) => console.log(order.number, order.previous, '→', order.status))
store.on('session', ({ token }) => { session = token })
const product = await store.emitWithAck('getProduct', { id: 1 })
console.log(product.data.name)
const made = await store.emitWithAck('createCategory', { name: 'Garden', slug: 'garden' })
console.log(made.ok ? made.data.id : made.error.message)
// a later connection picks the writes up
const again = io('https://api.sondahub.com/store', { transports: ['websocket'], auth: { session } })
From any browser console, with nothing installed:
const { io } = await import('https://cdn.jsdelivr.net/npm/[email protected]/+esm')
const s = io('https://api.sondahub.com', { transports: ['websocket'] })
s.on('tick', (t) => console.log('tick', t.n))
console.log(await s.emitWithAck('echo', 'hello'))
s.emit('clock', { count: 5, interval: 500 })
Python (python-socketio)
import socketio # pip install "python-socketio[client]"
sio = socketio.Client()
@sio.on('telemetry', namespace='/fleet')
def telemetry(reading):
print(reading['serial'], reading['metrics'])
sio.connect('https://api.sondahub.com', namespaces=['/store', '/fleet'], transports=['websocket'])
product = sio.call('getProduct', {'id': 1}, namespace='/store')
print(product['data']['name'])
sio.wait()
The test events
Every namespace answers these — on / and /admin they are all there is.
| Emit | With | Answer | What it does |
|---|---|---|---|
echo | …anything | the same arguments | Answers exactly what it was sent, binary included. |
time | data: { iso, unix, ms } | The server clock. | |
sleep | ms | data: { slept } | Answers after ms milliseconds (up to 10000) — for trying a client’s timeout. |
fail | { code, message, data } | ok: false, error: what you asked for | Answers with the error you ask for. |
ask | text | data: { asked, ack_id } | Then the server emits "question" to you, with an ack; acknowledge it and it emits "answer" with what you sent back and how long you took. |
clock | { count, interval } | data: { count, interval } | Then emits "tick" { n, of, ts } count times (up to 100, every 100–10000 ms), and "done". |
binary | size | data: <bytes> | size bytes of binary (up to 100,000): 0, 1, 2 … 255, 0, 1 … |
kick | data: { kicked } | Then disconnects you from the namespace: the client sees "io server disconnect" and does not reconnect by itself. | |
drop | data: { dropping } | Then cuts the connection: the client sees "transport close" and reconnects by itself. | |
help | data: { events, … } | What this namespace answers. |
/admin adds whoami: how you got in, and the token's claims.
The API namespaces
Each of the seven APIs is a namespace with five calls per collection — the same calls, data and rules as the REST API, so a transfer checks the funds and an order refuses an impossible status. /store has listCategories, getCategory, createCategory, updateCategory, deleteCategory and so on for every collection; emit help for the whole list.
| Emit | With | Answer | What it does |
|---|---|---|---|
list{Things} | { page, limit, sort, q, filter } | data: { data, page, limit, total, pages } | A page, filtered, searched and sorted like the REST list. |
get{Thing} | { id } or id | data: the record | One record. |
create{Thing} | { …fields } | data: the record | Validated and run through the same rules as REST, kept for the connection; a "session" event follows. |
update{Thing} | { id, …fields } or id, { …fields } | data: the record | Changes the fields given. |
delete{Thing} | { id } or id | data: what was removed | Deletes one. |
subscribe | topic or [topics] | data: { topics } | Add live topics. |
unsubscribe | topic or [topics] | data: { topics } | Drop live topics. |
topics | data: { available, subscribed } | What the namespace pushes, and what you hear. |
What the server pushes
On joining, a welcome event: the namespace's events and topics. Then the API's own activity, about once a second, as events named for the topic. Narrow them as you join with auth: { topics: ['orders'] } (an empty list for none), or later with subscribe and unsubscribe.
| Namespace | Events |
|---|---|
/store | orders 4 s · inventory 6 s |
/fleet | telemetry 2 s · alerts ~15 s · devices ~10 s |
/bank | fx 1 s · transactions 3 s |
/social | posts 5 s · likes 2 s |
/helpdesk | tickets 5 s · messages 4 s |
/flights | board 3 s · bookings 6 s |
/identity | directory ~6 s · logins 2 s |
connect_error, on purpose
const admin = io('https://api.sondahub.com/admin', { transports: ['websocket'], auth: { token: 'wrong' } })
admin.on('connect_error', (err) => console.log(err.message, err.data))
// → not authorized { reason: 'the token is neither the API key nor a JWT', hint: '…' }
const ok = io('https://api.sondahub.com/admin', { transports: ['websocket'], auth: { token: 'sonda-probe-key' } })
console.log(await ok.emitWithAck('whoami')) // { ok: true, data: { via: 'api key', … } }
An unknown namespace is refused as a real server refuses it, Invalid namespace — with the namespaces that exist in err.data. Unknown topics in auth are refused the same way.
Socket.IO as it is written
- The handshake and heartbeat. The Engine.IO open packet with its own sid, a ping every 25 seconds and 20 for the pong, after which the connection is closed; 1,000,000 bytes per message at most. A connection that joins no namespace within 45 seconds is closed, as a real server closes it.
- Acknowledgements both ways. Yours are answered;
askmakes the server send you an event with an acknowledgement of its own, and it reports what you sent back. - Binary. Buffers anywhere in the arguments travel as attachments, up to 10 a packet, and come back as binary —
echoone, or askbinaryfor some. - Errors close the connection, as on a real server: a packet that cannot be read, an event for a namespace you have not joined, a reserved event name (
connect,disconnect…), a ping from the client (in Engine.IO 4 the server pings). So does JSON nested more than 32 levels deep. - Limits. 20 events a second per connection, in bursts of 100: past that, an event with an acknowledgement is answered
rate_limitedand one without is dropped.sleepwaits 10 seconds at most. A connection's writes stop at what a session token can carry (session_full; deletes still work).
Questions
Why WebSocket only?
A Socket.IO client starts with HTTP long-polling by default, and polling needs the server to remember each client between separate requests. sondahub keeps nothing between requests — no database, no shared memory — so it serves the WebSocket transport, where the whole conversation lives in one connection. Set transports: ['websocket'] (Python: transports=['websocket']). A polling request is answered as a real server set up that way answers it: 400 {"code":0,"message":"Transport unknown"}.
Which clients work?
Socket.IO 3 and 4 clients — the JavaScript client, python-socketio 5, the Swift, Java, Dart and C++ clients of those versions — and API clients that speak Socket.IO over WebSocket. Version 2 clients speak Engine.IO 3 and are refused, as a current server refuses them unless told otherwise.
Can two clients talk to each other — rooms, broadcasts?
No. Every connection is its own world: what you emit is answered to you, and the live events are generated for your connection alone. A chat between two browsers needs a server that holds both connections, which a no-database playground cannot be.
Do writes stick?
For the life of the connection, yes: a create, update or delete is validated and run through the same rules as the REST API, and later calls on the same connection see it. After each write a session event carries the token holding your changes; connect again with auth: { session: token } (or ?_session= in the query) and they are still there. How sessions work.
How do I try connect_error and reconnection?
Connect to /admin without a token, or with a wrong one: the server refuses with connect_error, its message and a data object saying why. sonda-probe-key lets you in, and so does any JWT the hub signs (from the OAuth server or the JWT endpoints). Emit kick to be disconnected by the server (no automatic reconnect), drop to have the connection cut (the client reconnects by itself).