Skip to content

Commit 36778e9

Browse files
committed
feat(errors): enhance WebSocket error output and abstract connection setup
- Add DeepgramWebSocketError class with detailed debugging info including HTTP status codes, Deepgram request IDs, response headers, and connection state - Abstract common connection event setup pattern into reusable setupConnectionEvents() method in AbstractLiveClient - Update all live clients (Listen, Agent, Speak) to use abstracted pattern - Eliminate ~45 lines of duplicated connection event handling code - Maintain full backward compatibility with existing error event handlers Addresses customer feedback about difficulty debugging "non-101 status code" errors by exposing the actual HTTP status and request ID that were previously hidden behind generic Node.js WebSocket error messages.
1 parent 7924959 commit 36778e9

5 files changed

Lines changed: 327 additions & 37 deletions

File tree

src/lib/errors.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,55 @@ export class DeepgramVersionError extends DeepgramError {
4848
this.name = "DeepgramVersionError";
4949
}
5050
}
51+
52+
/**
53+
* Enhanced WebSocket error that captures additional debugging information
54+
* including status codes, request IDs, and response headers when available.
55+
*/
56+
export class DeepgramWebSocketError extends DeepgramError {
57+
originalEvent?: ErrorEvent | Event;
58+
statusCode?: number;
59+
requestId?: string;
60+
responseHeaders?: Record<string, string>;
61+
url?: string;
62+
readyState?: number;
63+
64+
constructor(
65+
message: string,
66+
options: {
67+
originalEvent?: ErrorEvent | Event;
68+
statusCode?: number;
69+
requestId?: string;
70+
responseHeaders?: Record<string, string>;
71+
url?: string;
72+
readyState?: number;
73+
} = {}
74+
) {
75+
super(message);
76+
this.name = "DeepgramWebSocketError";
77+
this.originalEvent = options.originalEvent;
78+
this.statusCode = options.statusCode;
79+
this.requestId = options.requestId;
80+
this.responseHeaders = options.responseHeaders;
81+
this.url = options.url;
82+
this.readyState = options.readyState;
83+
}
84+
85+
toJSON() {
86+
return {
87+
name: this.name,
88+
message: this.message,
89+
statusCode: this.statusCode,
90+
requestId: this.requestId,
91+
responseHeaders: this.responseHeaders,
92+
url: this.url,
93+
readyState: this.readyState,
94+
originalEvent: this.originalEvent
95+
? {
96+
type: this.originalEvent.type,
97+
timeStamp: this.originalEvent.timeStamp,
98+
}
99+
: undefined,
100+
};
101+
}
102+
}

src/packages/AbstractLiveClient.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CONNECTION_STATE, SOCKET_STATES } from "../lib/constants";
33
import type { DeepgramClientOptions, LiveSchema } from "../lib/types";
44
import type { WebSocket as WSWebSocket } from "ws";
55
import { isBun } from "../lib/runtime";
6+
import { DeepgramWebSocketError } from "../lib/errors";
67

78
/**
89
* Represents a constructor for a WebSocket-like object that can be used in the application.
@@ -285,6 +286,235 @@ export abstract class AbstractLiveClient extends AbstractClient {
285286
return this.key === "proxy" && !!this.namespaceOptions.websocket.options.proxy?.url;
286287
}
287288

289+
/**
290+
* Extracts enhanced error information from a WebSocket error event.
291+
* This method attempts to capture additional debugging information such as
292+
* status codes, request IDs, and response headers when available.
293+
*
294+
* @example
295+
* ```typescript
296+
* // Enhanced error information is now available in error events:
297+
* connection.on(LiveTranscriptionEvents.Error, (err) => {
298+
* console.error("WebSocket Error:", err.message);
299+
*
300+
* // Access HTTP status code (e.g., 502, 403, etc.)
301+
* if (err.statusCode) {
302+
* console.error(`HTTP Status Code: ${err.statusCode}`);
303+
* }
304+
*
305+
* // Access Deepgram request ID for support tickets
306+
* if (err.requestId) {
307+
* console.error(`Deepgram Request ID: ${err.requestId}`);
308+
* }
309+
*
310+
* // Access WebSocket URL and connection state
311+
* if (err.url) {
312+
* console.error(`WebSocket URL: ${err.url}`);
313+
* }
314+
*
315+
* if (err.readyState !== undefined) {
316+
* const stateNames = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
317+
* console.error(`Connection State: ${stateNames[err.readyState]}`);
318+
* }
319+
*
320+
* // Access response headers for additional debugging
321+
* if (err.responseHeaders) {
322+
* console.error("Response Headers:", err.responseHeaders);
323+
* }
324+
*
325+
* // Access the enhanced error object for detailed debugging
326+
* if (err.error?.name === 'DeepgramWebSocketError') {
327+
* console.error("Enhanced Error Details:", err.error.toJSON());
328+
* }
329+
* });
330+
* ```
331+
*
332+
* @param event - The error event from the WebSocket
333+
* @param conn - The WebSocket connection object
334+
* @returns Enhanced error information object
335+
*/
336+
protected extractErrorInformation(
337+
event: ErrorEvent | Event,
338+
conn?: WebSocketLike
339+
): {
340+
statusCode?: number;
341+
requestId?: string;
342+
responseHeaders?: Record<string, string>;
343+
url?: string;
344+
readyState?: number;
345+
} {
346+
const errorInfo: {
347+
statusCode?: number;
348+
requestId?: string;
349+
responseHeaders?: Record<string, string>;
350+
url?: string;
351+
readyState?: number;
352+
} = {};
353+
354+
// Extract basic connection information
355+
if (conn) {
356+
errorInfo.readyState = conn.readyState;
357+
errorInfo.url = typeof conn.url === "string" ? conn.url : conn.url?.toString();
358+
}
359+
360+
// Try to extract additional information from the WebSocket connection
361+
// This works with the 'ws' package which exposes more detailed error information
362+
if (conn && typeof conn === "object") {
363+
const wsConn = conn as any;
364+
365+
// Extract status code if available (from 'ws' package)
366+
if (wsConn._req && wsConn._req.res) {
367+
errorInfo.statusCode = wsConn._req.res.statusCode;
368+
369+
// Extract response headers if available
370+
if (wsConn._req.res.headers) {
371+
errorInfo.responseHeaders = { ...wsConn._req.res.headers };
372+
373+
// Extract request ID from Deepgram response headers
374+
const requestId =
375+
wsConn._req.res.headers["dg-request-id"] || wsConn._req.res.headers["x-dg-request-id"];
376+
if (requestId) {
377+
errorInfo.requestId = requestId;
378+
}
379+
}
380+
}
381+
382+
// For native WebSocket, try to extract information from the event
383+
if (event && "target" in event && event.target) {
384+
const target = event.target as any;
385+
if (target.url) {
386+
errorInfo.url = target.url;
387+
}
388+
if (target.readyState !== undefined) {
389+
errorInfo.readyState = target.readyState;
390+
}
391+
}
392+
}
393+
394+
return errorInfo;
395+
}
396+
397+
/**
398+
* Creates an enhanced error object with additional debugging information.
399+
* This method provides backward compatibility by including both the original
400+
* error event and enhanced error information.
401+
*
402+
* @param event - The original error event
403+
* @param enhancedInfo - Additional error information extracted from the connection
404+
* @returns An object containing both original and enhanced error information
405+
*/
406+
protected createEnhancedError(
407+
event: ErrorEvent | Event,
408+
enhancedInfo: {
409+
statusCode?: number;
410+
requestId?: string;
411+
responseHeaders?: Record<string, string>;
412+
url?: string;
413+
readyState?: number;
414+
}
415+
) {
416+
// Create the enhanced error for detailed debugging
417+
const enhancedError = new DeepgramWebSocketError(
418+
(event as ErrorEvent).message || "WebSocket connection error",
419+
{
420+
originalEvent: event,
421+
...enhancedInfo,
422+
}
423+
);
424+
425+
// Return an object that maintains backward compatibility
426+
// while providing enhanced information
427+
return {
428+
// Original event for backward compatibility
429+
...event,
430+
// Enhanced error information
431+
error: enhancedError,
432+
// Additional fields for easier access
433+
statusCode: enhancedInfo.statusCode,
434+
requestId: enhancedInfo.requestId,
435+
responseHeaders: enhancedInfo.responseHeaders,
436+
url: enhancedInfo.url,
437+
readyState: enhancedInfo.readyState,
438+
// Enhanced message with more context
439+
message: this.buildEnhancedErrorMessage(event, enhancedInfo),
440+
};
441+
}
442+
443+
/**
444+
* Builds an enhanced error message with additional context information.
445+
*
446+
* @param event - The original error event
447+
* @param enhancedInfo - Additional error information
448+
* @returns A more descriptive error message
449+
*/
450+
protected buildEnhancedErrorMessage(
451+
event: ErrorEvent | Event,
452+
enhancedInfo: {
453+
statusCode?: number;
454+
requestId?: string;
455+
responseHeaders?: Record<string, string>;
456+
url?: string;
457+
readyState?: number;
458+
}
459+
): string {
460+
let message = (event as ErrorEvent).message || "WebSocket connection error";
461+
462+
const details: string[] = [];
463+
464+
if (enhancedInfo.statusCode) {
465+
details.push(`Status: ${enhancedInfo.statusCode}`);
466+
}
467+
468+
if (enhancedInfo.requestId) {
469+
details.push(`Request ID: ${enhancedInfo.requestId}`);
470+
}
471+
472+
if (enhancedInfo.readyState !== undefined) {
473+
const stateNames = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
474+
const stateName =
475+
stateNames[enhancedInfo.readyState] || `Unknown(${enhancedInfo.readyState})`;
476+
details.push(`Ready State: ${stateName}`);
477+
}
478+
479+
if (enhancedInfo.url) {
480+
details.push(`URL: ${enhancedInfo.url}`);
481+
}
482+
483+
if (details.length > 0) {
484+
message += ` (${details.join(", ")})`;
485+
}
486+
487+
return message;
488+
}
489+
490+
/**
491+
* Sets up the standard connection event handlers (open, close, error) for WebSocket connections.
492+
* This method abstracts the common connection event registration pattern used across all live clients.
493+
*
494+
* @param events - Object containing the event constants for the specific client type
495+
* @param events.Open - Event constant for connection open
496+
* @param events.Close - Event constant for connection close
497+
* @param events.Error - Event constant for connection error
498+
* @protected
499+
*/
500+
protected setupConnectionEvents(events: { Open: string; Close: string; Error: string }): void {
501+
if (this.conn) {
502+
this.conn.onopen = () => {
503+
this.emit(events.Open, this);
504+
};
505+
506+
this.conn.onclose = (event: any) => {
507+
this.emit(events.Close, event);
508+
};
509+
510+
this.conn.onerror = (event: ErrorEvent) => {
511+
const enhancedInfo = this.extractErrorInformation(event, this.conn || undefined);
512+
const enhancedError = this.createEnhancedError(event, enhancedInfo);
513+
this.emit(events.Error, enhancedError);
514+
};
515+
}
516+
}
517+
288518
/**
289519
* Sets up the connection event handlers.
290520
*

src/packages/AgentLiveClient.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,15 @@ export class AgentLiveClient extends AbstractLiveClient {
2222
* - When a message is received, it parses the message and emits the appropriate event based on the message type.
2323
*/
2424
public setupConnection(): void {
25-
if (this.conn) {
26-
this.conn.onopen = () => {
27-
this.emit(AgentEvents.Open, this);
28-
};
29-
30-
this.conn.onclose = (event: any) => {
31-
this.emit(AgentEvents.Close, event);
32-
};
33-
34-
this.conn.onerror = (event: ErrorEvent) => {
35-
this.emit(AgentEvents.Error, event);
36-
};
25+
// Set up standard connection events (open, close, error) using abstracted method
26+
this.setupConnectionEvents({
27+
Open: AgentEvents.Open,
28+
Close: AgentEvents.Close,
29+
Error: AgentEvents.Error,
30+
});
3731

32+
// Set up message handling specific to agent conversations
33+
if (this.conn) {
3834
this.conn.onmessage = (event: MessageEvent) => {
3935
this.handleMessage(event);
4036
};
@@ -53,9 +49,13 @@ export class AgentLiveClient extends AbstractLiveClient {
5349
} catch (error) {
5450
this.emit(AgentEvents.Error, {
5551
event,
56-
data: event.data,
52+
data:
53+
event.data?.toString().substring(0, 200) +
54+
(event.data?.toString().length > 200 ? "..." : ""),
5755
message: "Unable to parse `data` as JSON.",
5856
error,
57+
url: this.conn?.url,
58+
readyState: this.conn?.readyState,
5959
});
6060
}
6161
} else if (event.data instanceof Blob) {
@@ -71,6 +71,9 @@ export class AgentLiveClient extends AbstractLiveClient {
7171
this.emit(AgentEvents.Error, {
7272
event,
7373
message: "Received unknown data type.",
74+
url: this.conn?.url,
75+
readyState: this.conn?.readyState,
76+
dataType: typeof event.data,
7477
});
7578
}
7679
}

src/packages/ListenLiveClient.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,15 @@ export class ListenLiveClient extends AbstractLiveClient {
5151
* - When a message is received, it parses the message and emits the appropriate event based on the message type, such as `LiveTranscriptionEvents.Metadata`, `LiveTranscriptionEvents.Transcript`, `LiveTranscriptionEvents.UtteranceEnd`, and `LiveTranscriptionEvents.SpeechStarted`.
5252
*/
5353
public setupConnection(): void {
54-
if (this.conn) {
55-
this.conn.onopen = () => {
56-
this.emit(LiveTranscriptionEvents.Open, this);
57-
};
58-
59-
this.conn.onclose = (event: any) => {
60-
this.emit(LiveTranscriptionEvents.Close, event);
61-
};
62-
63-
this.conn.onerror = (event: ErrorEvent) => {
64-
this.emit(LiveTranscriptionEvents.Error, event);
65-
};
54+
// Set up standard connection events (open, close, error) using abstracted method
55+
this.setupConnectionEvents({
56+
Open: LiveTranscriptionEvents.Open,
57+
Close: LiveTranscriptionEvents.Close,
58+
Error: LiveTranscriptionEvents.Error,
59+
});
6660

61+
// Set up message handling specific to transcription
62+
if (this.conn) {
6763
this.conn.onmessage = (event: MessageEvent) => {
6864
try {
6965
const data: any = JSON.parse(event.data.toString());
@@ -84,6 +80,11 @@ export class ListenLiveClient extends AbstractLiveClient {
8480
event,
8581
message: "Unable to parse `data` as JSON.",
8682
error,
83+
url: this.conn?.url,
84+
readyState: this.conn?.readyState,
85+
data:
86+
event.data?.toString().substring(0, 200) +
87+
(event.data?.toString().length > 200 ? "..." : ""),
8788
});
8889
}
8990
};

0 commit comments

Comments
 (0)