{"record":{"id":"512a04ec69f11ddc","repo":"stablyai/orca","slug":"expected-plaintext-relay-hello","errorCode":null,"errorMessage":"expected plaintext relay hello","messagePattern":"expected plaintext relay hello","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mobile/src/transport/mobile-relay-e2ee-link.ts","lineNumber":108,"sourceCode":"        .then(async () => {\n          if (this.closed) {\n            return\n          }\n          if (!this.outerReady) {\n            this.acceptHello(event.data)\n          } else {\n            await this.channel.handleMessage(event.data)\n          }\n        })\n        .catch((error: unknown) => this.fail(asError(error)))\n    }\n    this.socket.onerror = () => this.fail(new Error('relay transport error'))\n    this.socket.onclose = (event) => this.fail(new RelayOuterError(event.code || 1006))\n  }\n\n  private acceptHello(raw: unknown): void {\n    if (typeof raw !== 'string') {\n      throw new Error('expected plaintext relay hello')\n    }\n    let value: unknown\n    try {\n      value = JSON.parse(raw)\n    } catch {\n      throw new Error('invalid relay hello JSON')\n    }\n    const parsed = RelayPhoneHelloSchema.safeParse(value)\n    if (!parsed.success) {\n      throw new Error('invalid relay hello')\n    }\n    if (!parsed.data.ok) {\n      throw new RelayOuterError(parsed.data.code)\n    }\n    if (parsed.data.credentialKind !== this.options.expectedCredentialKind) {\n      throw new Error('relay credential resolved as an unexpected credential kind')\n    }\n    this.outerReady = true","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/transport/mobile-relay-e2ee-link.ts#L90-L126","documentation":"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.","triggerScenarios":"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.","commonSituations":"Relay version that switched to binary framing; React Native WebSocket defaulting to arraybuffer; proxy that re-encodes text frames as binary.","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."],"exampleFix":"// before\nthis.socket.onmessage = (event) => { if (!outerReady) this.acceptHello(event.data) } // throws on ArrayBuffer\n\n// after\nthis.socket.onmessage = (event) => {\n  const raw = event.data instanceof ArrayBuffer ? new TextDecoder().decode(event.data) : event.data\n  if (!this.outerReady) this.acceptHello(raw)\n  else this.channel.handleMessage(event.data)\n}","handlingStrategy":"validation","validationCode":"function isTextHello(raw: unknown): raw is string { return typeof raw === 'string' }","typeGuard":"function isRelayText(raw: unknown): raw is string { return typeof raw === 'string' }","tryCatchPattern":"try { this.acceptHello(firstFrame) } catch (e) { if (e.message === 'expected plaintext relay hello') { /* coerce or reconnect with text framing */ } else throw e }","preventionTips":["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."],"tags":["relay","websocket","handshake","framing"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}