Skip to content

Commit e9fc4da

Browse files
committed
Improve error code formatting
1 parent fc7c4c6 commit e9fc4da

14 files changed

Lines changed: 248 additions & 80 deletions

File tree

src/base-command.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./services/config-manager.js";
1111
import { ControlApi } from "./services/control-api.js";
1212
import { CommandError } from "./errors/command-error.js";
13+
import { getFriendlyAblyErrorHint } from "./utils/errors.js";
1314
import { coreGlobalFlags } from "./flags.js";
1415
import { InteractiveHelper } from "./services/interactive-helper.js";
1516
import { BaseFlags, CommandConfig } from "./types/cli.js";
@@ -936,9 +937,10 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
936937
logData,
937938
);
938939
} else if (level <= 1) {
939-
// Standard Non-JSON: Log only SDK ERRORS (level <= 1) clearly
940-
// Use a format similar to logCliEvent's non-JSON output
941-
this.log(`${chalk.red.bold(`[AblySDK Error]`)} ${message}`);
940+
// SDK errors are handled by setupChannelStateLogging() and fail()
941+
// Only show raw SDK errors in verbose mode (handled above)
942+
// In non-verbose mode, log to stderr for debugging without polluting stdout
943+
this.logToStderr(`${chalk.red.bold(`[AblySDK Error]`)} ${message}`);
942944
}
943945
// If not verbose non-JSON and level > 1, suppress non-error SDK logs
944946
}
@@ -1503,6 +1505,11 @@ export abstract class AblyBaseCommand extends InteractiveBaseCommand {
15031505
}
15041506

15051507
let humanMessage = cmdError.message;
1508+
const friendlyHint = getFriendlyAblyErrorHint(cmdError.code);
1509+
if (friendlyHint) {
1510+
humanMessage += `\n${friendlyHint}`;
1511+
}
1512+
15061513
const code = cmdError.code ?? cmdError.context.errorCode;
15071514
if (code !== undefined) {
15081515
const helpUrl = cmdError.context.helpUrl;

src/commands/channels/occupancy/subscribe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default class ChannelsOccupancySubscribe extends AblyBaseCommand {
8686
);
8787
}
8888

89-
channel.subscribe(occupancyEventName, (message: Ably.Message) => {
89+
await channel.subscribe(occupancyEventName, (message: Ably.Message) => {
9090
const timestamp = formatMessageTimestamp(message.timestamp);
9191
const event = {
9292
channel: channelName,

src/commands/channels/presence/enter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export default class ChannelsPresenceEnter extends AblyBaseCommand {
104104

105105
// Subscribe to presence events before entering (if show-others is enabled)
106106
if (flags["show-others"]) {
107-
this.channel.presence.subscribe((presenceMessage) => {
107+
await this.channel.presence.subscribe((presenceMessage) => {
108108
// Filter out own presence events
109109
if (presenceMessage.clientId === client.auth.clientId) {
110110
return;

src/commands/channels/presence/subscribe.ts

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -79,41 +79,43 @@ export default class ChannelsPresenceSubscribe extends AblyBaseCommand {
7979
);
8080
}
8181

82-
channel.presence.subscribe((presenceMessage: Ably.PresenceMessage) => {
83-
const timestamp = formatMessageTimestamp(presenceMessage.timestamp);
84-
const presenceData = {
85-
id: presenceMessage.id,
86-
timestamp,
87-
action: presenceMessage.action,
88-
channel: channelName,
89-
clientId: presenceMessage.clientId,
90-
connectionId: presenceMessage.connectionId,
91-
data: presenceMessage.data,
92-
};
93-
this.logCliEvent(
94-
flags,
95-
"presence",
96-
presenceMessage.action!,
97-
`Presence event: ${presenceMessage.action} by ${presenceMessage.clientId}`,
98-
presenceData,
99-
);
100-
101-
if (this.shouldOutputJson(flags)) {
102-
this.logJsonEvent({ presenceMessage: presenceData }, flags);
103-
} else {
104-
const displayFields: PresenceDisplayFields = {
82+
await channel.presence.subscribe(
83+
(presenceMessage: Ably.PresenceMessage) => {
84+
const timestamp = formatMessageTimestamp(presenceMessage.timestamp);
85+
const presenceData = {
10586
id: presenceMessage.id,
106-
timestamp: presenceMessage.timestamp ?? Date.now(),
107-
action: presenceMessage.action || "unknown",
87+
timestamp,
88+
action: presenceMessage.action,
10889
channel: channelName,
10990
clientId: presenceMessage.clientId,
11091
connectionId: presenceMessage.connectionId,
11192
data: presenceMessage.data,
11293
};
113-
this.log(formatPresenceOutput([displayFields]));
114-
this.log(""); // Empty line for better readability
115-
}
116-
});
94+
this.logCliEvent(
95+
flags,
96+
"presence",
97+
presenceMessage.action!,
98+
`Presence event: ${presenceMessage.action} by ${presenceMessage.clientId}`,
99+
presenceData,
100+
);
101+
102+
if (this.shouldOutputJson(flags)) {
103+
this.logJsonEvent({ presenceMessage: presenceData }, flags);
104+
} else {
105+
const displayFields: PresenceDisplayFields = {
106+
id: presenceMessage.id,
107+
timestamp: presenceMessage.timestamp ?? Date.now(),
108+
action: presenceMessage.action || "unknown",
109+
channel: channelName,
110+
clientId: presenceMessage.clientId,
111+
connectionId: presenceMessage.connectionId,
112+
data: presenceMessage.data,
113+
};
114+
this.log(formatPresenceOutput([displayFields]));
115+
this.log(""); // Empty line for better readability
116+
}
117+
},
118+
);
117119

118120
if (!this.shouldOutputJson(flags)) {
119121
this.log(

src/commands/channels/subscribe.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export default class ChannelsSubscribe extends AblyBaseCommand {
154154
});
155155

156156
// Subscribe to messages on all channels
157-
const attachPromises: Promise<void>[] = [];
157+
const subscribePromises: Promise<unknown>[] = [];
158158

159159
for (const channel of channels) {
160160
this.logCliEvent(
@@ -177,19 +177,8 @@ export default class ChannelsSubscribe extends AblyBaseCommand {
177177
includeUserFriendlyMessages: true,
178178
});
179179

180-
// Track attachment promise
181-
const attachPromise = new Promise<void>((resolve) => {
182-
const checkAttached = () => {
183-
if (channel.state === "attached") {
184-
resolve();
185-
}
186-
};
187-
channel.once("attached", checkAttached);
188-
checkAttached(); // Check if already attached
189-
});
190-
attachPromises.push(attachPromise);
191-
192-
channel.subscribe((message: Ably.Message) => {
180+
// Subscribe and collect promise (rejects on capability/auth errors)
181+
const subscribePromise = channel.subscribe((message: Ably.Message) => {
193182
this.sequenceCounter++;
194183
const timestamp = formatMessageTimestamp(message.timestamp);
195184
const messageData = {
@@ -251,10 +240,11 @@ export default class ChannelsSubscribe extends AblyBaseCommand {
251240
this.log(""); // Empty line for readability between messages
252241
}
253242
});
243+
subscribePromises.push(subscribePromise);
254244
}
255245

256-
// Wait for all channels to attach
257-
await Promise.all(attachPromises);
246+
// Wait for all channels to attach via subscribe
247+
await Promise.all(subscribePromises);
258248

259249
// Log the ready signal for E2E tests
260250
if (channelNames.length === 1 && !this.shouldOutputJson(flags)) {

src/commands/logs/channel-lifecycle/subscribe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export default class LogsChannelLifecycleSubscribe extends AblyBaseCommand {
8888
}
8989

9090
// Subscribe to the channel
91-
channel.subscribe((message) => {
91+
await channel.subscribe((message) => {
9292
const timestamp = formatMessageTimestamp(message.timestamp);
9393
const event = message.name || "unknown";
9494
const logEvent = {

src/commands/logs/connection-lifecycle/subscribe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export default class LogsConnectionLifecycleSubscribe extends AblyBaseCommand {
8585
}
8686

8787
// Subscribe to connection lifecycle logs
88-
channel.subscribe((message: Ably.Message) => {
88+
await channel.subscribe((message: Ably.Message) => {
8989
const timestamp = formatMessageTimestamp(message.timestamp);
9090
const event = {
9191
timestamp,

src/commands/logs/push/subscribe.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default class LogsPushSubscribe extends AblyBaseCommand {
7979
);
8080

8181
// Subscribe to the channel
82-
channel.subscribe((message) => {
82+
await channel.subscribe((message) => {
8383
const timestamp = formatMessageTimestamp(message.timestamp);
8484
const event = message.name || "unknown";
8585
const logEvent = {

src/commands/logs/subscribe.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -120,41 +120,46 @@ export default class LogsSubscribe extends AblyBaseCommand {
120120
}
121121

122122
// Subscribe to specified log types
123+
const subscribePromises: Promise<unknown>[] = [];
123124
for (const logType of logTypes) {
124-
channel.subscribe(logType, (message: Ably.Message) => {
125-
const timestamp = formatMessageTimestamp(message.timestamp);
126-
const event = {
127-
logType,
128-
timestamp,
129-
data: message.data,
130-
id: message.id,
131-
};
132-
this.logCliEvent(
133-
flags,
134-
"logs",
135-
"logReceived",
136-
`Log received: ${logType}`,
137-
event,
138-
);
139-
140-
if (this.shouldOutputJson(flags)) {
141-
this.logJsonEvent(event, flags);
142-
} else {
143-
this.log(
144-
`${formatTimestamp(timestamp)} Type: ${formatEventType(logType)}`,
125+
subscribePromises.push(
126+
channel.subscribe(logType, (message: Ably.Message) => {
127+
const timestamp = formatMessageTimestamp(message.timestamp);
128+
const event = {
129+
logType,
130+
timestamp,
131+
data: message.data,
132+
id: message.id,
133+
};
134+
this.logCliEvent(
135+
flags,
136+
"logs",
137+
"logReceived",
138+
`Log received: ${logType}`,
139+
event,
145140
);
146141

147-
if (message.data !== null && message.data !== undefined) {
142+
if (this.shouldOutputJson(flags)) {
143+
this.logJsonEvent(event, flags);
144+
} else {
148145
this.log(
149-
`${formatLabel("Data")} ${JSON.stringify(message.data, null, 2)}`,
146+
`${formatTimestamp(timestamp)} Type: ${formatEventType(logType)}`,
150147
);
151-
}
152148

153-
this.log(""); // Empty line for better readability
154-
}
155-
});
149+
if (message.data !== null && message.data !== undefined) {
150+
this.log(
151+
`${formatLabel("Data")} ${JSON.stringify(message.data, null, 2)}`,
152+
);
153+
}
154+
155+
this.log(""); // Empty line for better readability
156+
}
157+
}),
158+
);
156159
}
157160

161+
await Promise.all(subscribePromises);
162+
158163
this.logCliEvent(
159164
flags,
160165
"logs",

src/utils/errors.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,29 @@
44
export function errorMessage(error: unknown): string {
55
return error instanceof Error ? error.message : String(error);
66
}
7+
8+
/**
9+
* Return a friendly, actionable hint for known Ably error codes.
10+
* Returns undefined for unknown codes.
11+
*/
12+
export function getFriendlyAblyErrorHint(
13+
code: number | undefined,
14+
): string | undefined {
15+
if (code === undefined) return undefined;
16+
17+
const hints: Record<number, string> = {
18+
40101:
19+
'The credentials provided are not valid. Check your API key or token, or re-authenticate with "ably login".',
20+
40103: 'The token has expired. Please re-authenticate with "ably login".',
21+
40110:
22+
'Unable to authorize. Check your authentication configuration or re-authenticate with "ably login".',
23+
40160:
24+
"The API key does not have the capability to perform this operation on the requested resource. Check the key's channel capability configuration in the Ably dashboard.",
25+
40161:
26+
"The API key does not have publish capability on this resource. Check the key's channel capability configuration in the Ably dashboard.",
27+
40171:
28+
"The requested operation is not permitted by the API key's capabilities. Check the key's capability configuration in the Ably dashboard.",
29+
};
30+
31+
return hints[code];
32+
}

0 commit comments

Comments
 (0)