Skip to content

Commit 25f3fea

Browse files
fix(core): enhance zodSocket protocol detection to prevent message corruption
Fixed a critical bug where user messages containing a 'payload' property were incorrectly identified as protocol-wrapped messages, causing data loss. Changes: - Enhanced hasValidPayload check to require both 'payload' AND 'version' - Added isObject() helper to exclude arrays and null values - Refactored message normalization into dedicated normalizeMessage() method - Added comprehensive test suite with 11 test cases covering all edge cases The fix ensures that only messages with the full protocol signature (version + payload as object) are treated as wrapped messages. Fixes: Protocol ambiguity where user data with 'payload' field was misinterpreted Tests: 11/11 passing, including edge cases for non-object payloads and missing version
1 parent aa69b90 commit 25f3fea

File tree

2 files changed

+419
-8
lines changed

2 files changed

+419
-8
lines changed

packages/core/src/v3/zodSocket.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ type MessageFromSocketSchema<
7979
payload: z.input<GetSocketMessageSchema<TMessageCatalog, K>>;
8080
};
8181

82+
type NormalizedMessage = {
83+
type: string;
84+
version?: string;
85+
payload: Record<string, any>;
86+
};
87+
8288
export type MessagesFromSocketCatalog<TMessageCatalog extends ZodSocketMessageCatalogSchema> = {
8389
[K in keyof TMessageCatalog]: MessageFromSocketSchema<K, TMessageCatalog>;
8490
}[keyof TMessageCatalog];
@@ -187,6 +193,28 @@ export class ZodSocketMessageHandler<TRPCCatalog extends ZodSocketMessageCatalog
187193
};
188194
}
189195

196+
private isObject(value: any): value is Record<string, any> {
197+
return value !== null && typeof value === "object" && !Array.isArray(value);
198+
}
199+
200+
private normalizeMessage(eventName: string, message: any): NormalizedMessage {
201+
const hasValidPayload = "payload" in message && "version" in message && this.isObject(message.payload);
202+
203+
if (hasValidPayload) {
204+
return {
205+
type: eventName,
206+
...message,
207+
};
208+
}
209+
210+
const { version, ...rest } = message;
211+
return {
212+
type: eventName,
213+
version: version,
214+
payload: rest,
215+
};
216+
}
217+
190218
public registerHandlers(emitter: EventEmitterLike, logger?: StructuredLogger) {
191219
const log = logger ?? console;
192220

@@ -206,14 +234,8 @@ export class ZodSocketMessageHandler<TRPCCatalog extends ZodSocketMessageCatalog
206234
let ack;
207235

208236
try {
209-
// FIXME: this only works if the message doesn't have genuine payload prop
210-
if ("payload" in message) {
211-
ack = await this.handleMessage({ type: eventName, ...message });
212-
} else {
213-
// Handle messages not sent by ZodMessageSender
214-
const { version, ...payload } = message;
215-
ack = await this.handleMessage({ type: eventName, version, payload });
216-
}
237+
const normalized = this.normalizeMessage(eventName, message);
238+
ack = await this.handleMessage(normalized);
217239
} catch (error) {
218240
log.error("Error while handling message", {
219241
error:

0 commit comments

Comments
 (0)