tinyhumansai/openhuman · error · Error
[tunnel] no session cipher — handshake incomplete
Error message
[tunnel] no session cipher — handshake incomplete
What it means
sendEnvelope seals every frame with this.cipher, which exists only after the encrypted handshake completes and both sides derived the session key. This invariant fires when a request is sent before connect() resolved, or on a transport whose handshake failed and was reused anyway.
Source
Thrown at app/src/services/transport/TunnelTransport.ts:322
this.streams.delete(requestId);
}
return;
}
if (kind === 'response') {
const pending = this.pending.get(requestId);
if (!pending) return;
clearTimeout(pending.timeoutId);
this.pending.delete(requestId);
pending.resolve(envelope.payload);
return;
}
}
// -- send ------------------------------------------------------------------
private async sendEnvelope(envelope: Envelope): Promise<void> {
if (!this.cipher) throw new Error('[tunnel] no session cipher — handshake incomplete');
await this.rateLimiter.consume();
const chunks = chunk(envelope);
for (const raw of chunks) {
const encrypted = this.cipher.seal(raw);
const frameB64 = base64urlEncode(encrypted);
this.socket!.emit('tunnel:frame', { channelId: this.channelId, payload: frameB64 });
}
log(
'[tunnel] sent %s requestId=%s chunks=%d',
envelope.kind,
envelope.requestId,
chunks.length
);
}
View on GitHub (pinned to a221052e0d)
Solutions
- Always await connect() before the first call, or route through TransportManager which pings first
- On handshake/connect failure, close and discard the transport and build a fresh one
- Queue early requests until the handshake completes instead of sending immediately
Example fix
// before
void transport.connect();
await transport.call('openhuman.ping', {});
// after
await transport.connect();
await transport.call('openhuman.ping', {}); Defensive patterns
Strategy: validation
Validate before calling
// Gate sends on a completed round-trip instead of assuming connection:
const t = await manager.getTransport(); // ping already succeeded → handshake done
await t.call('openhuman.ping', {}); Try / catch
Catch 'handshake incomplete', close() the broken instance, construct a fresh transport, await connect(), and retry the request once — never resend on the same instance.
Prevention
- Never call a transport method before connect() resolves
- Treat handshake failure as fatal for that transport instance — replace it
- Queue requests that arrive during handshake rather than sending eagerly
When it happens
Trigger: Calling call()/request() on a TunnelTransport whose connect() promise has not resolved (or has rejected) — fire-and-forget connect() followed immediately by send; reconnect logic that swaps the socket but not the cipher.
Common situations: Missing `await transport.connect()`; racing requests fired right after construction; reusing a transport instance after a connect_error.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- [tunnel] no device keypair
- [transport:manager] tunnel profile missing channelId or core
- [transport:manager] tunnel profile missing sessionToken or p
- [transport:manager] all transports failed to connect
- [transport:cloud] ${method} timed out after ${this.timeoutMs
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/30cc10288c7abe03.
Report an issue: GitHub.