stablyai/orca · error
expected plaintext relay hello
Error message
expected plaintext relay hello
What it means
Thrown by `acceptHello` in `MobileRelayE2eeLink` when the very first WebSocket message from the relay (the relay phone hello) is not a string. The hello is plaintext JSON because no crypto keys exist yet; a binary payload means the relay framed the hello as binary, which the link cannot interpret at this stage.
Source
Thrown at mobile/src/transport/mobile-relay-e2ee-link.ts:108
.then(async () => {
if (this.closed) {
return
}
if (!this.outerReady) {
this.acceptHello(event.data)
} else {
await this.channel.handleMessage(event.data)
}
})
.catch((error: unknown) => this.fail(asError(error)))
}
this.socket.onerror = () => this.fail(new Error('relay transport error'))
this.socket.onclose = (event) => this.fail(new RelayOuterError(event.code || 1006))
}
private acceptHello(raw: unknown): void {
if (typeof raw !== 'string') {
throw new Error('expected plaintext relay hello')
}
let value: unknown
try {
value = JSON.parse(raw)
} catch {
throw new Error('invalid relay hello JSON')
}
const parsed = RelayPhoneHelloSchema.safeParse(value)
if (!parsed.success) {
throw new Error('invalid relay hello')
}
if (!parsed.data.ok) {
throw new RelayOuterError(parsed.data.code)
}
if (parsed.data.credentialKind !== this.options.expectedCredentialKind) {
throw new Error('relay credential resolved as an unexpected credential kind')
}
this.outerReady = trueView on GitHub (pinned to 1136503c6a)
Solutions
- Ensure the relay sends the hello as a text frame per the protocol.
- Coerce the first inbound frame to a string before `acceptHello` if the transport may deliver binary.
- In tests, pass a JSON string for the hello.
Example fix
// before
this.socket.onmessage = (event) => { if (!outerReady) this.acceptHello(event.data) } // throws on ArrayBuffer
// after
this.socket.onmessage = (event) => {
const raw = event.data instanceof ArrayBuffer ? new TextDecoder().decode(event.data) : event.data
if (!this.outerReady) this.acceptHello(raw)
else this.channel.handleMessage(event.data)
} Defensive patterns
Strategy: validation
Validate before calling
function isTextHello(raw: unknown): raw is string { return typeof raw === 'string' } Type guard
function isRelayText(raw: unknown): raw is string { return typeof raw === 'string' } Try / catch
try { this.acceptHello(firstFrame) } catch (e) { if (e.message === 'expected plaintext relay hello') { /* coerce or reconnect with text framing */ } else throw e } Prevention
- Ensure the relay sends the hello as a text frame.
- Coerce ArrayBuffer first frames to string at the transport boundary.
- Pass JSON strings for the hello in tests.
When it happens
Trigger: Relay WebSocket delivered the hello as an `ArrayBuffer`/`Blob`; relay implementation that always sends binary frames; `socket.binaryType` configured such that text frames arrive as non-string; a test feeding a buffer instead of JSON.
Common situations: Relay version that switched to binary framing; React Native WebSocket defaulting to arraybuffer; proxy that re-encodes text frames as binary.
Related errors
- Expected plaintext E2EE v2 ready
- invalid relay hello JSON
- invalid relay hello
- relay_outer_${code}
- expected plaintext relay hello
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/512a04ec69f11ddc.
Report an issue: GitHub.