sondahub

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' }
NamespaceWhat it does
/The test events: echo, time, sleep, fail, ask, clock, binary, kick, drop.
/adminThe same, behind auth: { token } — for trying connect_error.
/storeStore: 45 calls over acks, and live orders, inventory
/fleetFleet: 25 calls over acks, and live telemetry, alerts, devices
/bankBank: 30 calls over acks, and live fx, transactions
/socialSocial: 25 calls over acks, and live posts, likes
/helpdeskHelpdesk: 25 calls over acks, and live tickets, messages
/flightsFlights: 20 calls over acks, and live board, bookings
/identityIdentity: 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.

EmitWithAnswerWhat it does
echo…anythingthe same argumentsAnswers exactly what it was sent, binary included.
timedata: { iso, unix, ms }The server clock.
sleepmsdata: { slept }Answers after ms milliseconds (up to 10000) — for trying a client’s timeout.
fail{ code, message, data }ok: false, error: what you asked forAnswers with the error you ask for.
asktextdata: { 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".
binarysizedata: <bytes>size bytes of binary (up to 100,000): 0, 1, 2 … 255, 0, 1 …
kickdata: { kicked }Then disconnects you from the namespace: the client sees "io server disconnect" and does not reconnect by itself.
dropdata: { dropping }Then cuts the connection: the client sees "transport close" and reconnects by itself.
helpdata: { 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.

EmitWithAnswerWhat 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 iddata: the recordOne record.
create{Thing}{ …fields }data: the recordValidated 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 recordChanges the fields given.
delete{Thing}{ id } or iddata: what was removedDeletes one.
subscribetopic or [topics]data: { topics }Add live topics.
unsubscribetopic or [topics]data: { topics }Drop live topics.
topicsdata: { 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.

NamespaceEvents
/storeorders 4 s · inventory 6 s
/fleettelemetry 2 s · alerts ~15 s · devices ~10 s
/bankfx 1 s · transactions 3 s
/socialposts 5 s · likes 2 s
/helpdesktickets 5 s · messages 4 s
/flightsboard 3 s · bookings 6 s
/identitydirectory ~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; ask makes 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 — echo one, or ask binary for 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_limited and one without is dropped. sleep waits 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).