Skip to content

Commit 23ecf75

Browse files
committed
refactor: clean up cross-runtime error handling and add tests
1 parent 39b62a0 commit 23ecf75

File tree

6 files changed

+276
-79
lines changed

6 files changed

+276
-79
lines changed

genkit-tools/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"scripts": {
1818
"build": "pnpm genversion && tsc",
1919
"build:watch": "tsc --watch",
20-
"compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit",
20+
"compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit --minify",
2121
"test": "jest --verbose",
2222
"genversion": "genversion -esf src/utils/version.ts"
2323
},
@@ -47,6 +47,7 @@
4747
"genversion": "^3.2.0",
4848
"jest": "^29.7.0",
4949
"ts-jest": "^29.1.2",
50+
"ts-node": "^10.9.2",
5051
"typescript": "^5.3.3"
5152
}
5253
}

genkit-tools/cli/src/cli.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import { flowRun } from './commands/flow-run';
3131
import { getPluginCommands, getPluginSubCommand } from './commands/plugins';
3232
import { start } from './commands/start';
3333
import { uiStart } from './commands/ui-start';
34-
import { uiStartServer } from './commands/ui-start-server';
34+
import {
35+
UI_START_SERVER_COMMAND,
36+
uiStartServer,
37+
} from './commands/ui-start-server';
3538
import { uiStop } from './commands/ui-stop';
3639
import { version } from './utils/version';
3740

@@ -80,7 +83,9 @@ export async function startCLI(): Promise<void> {
8083
await record(new RunCommandEvent(commandName));
8184
});
8285

83-
if (process.argv.includes('__ui:start-server')) {
86+
// When running as a spawned UI server process, argv[1] will be '__ui:start-server'
87+
// instead of a normal command. This allows the same binary to serve both CLI and server roles.
88+
if (process.argv[1] === UI_START_SERVER_COMMAND) {
8489
program.addCommand(uiStartServer);
8590
}
8691

genkit-tools/cli/src/commands/ui-start-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ function redirectStdoutToFile(logFile: string) {
3232
process.stderr.write = process.stdout.write;
3333
}
3434

35+
export const UI_START_SERVER_COMMAND = '__ui:start-server' as const;
36+
3537
export const uiStartServer = new Command('__ui:start-server')
3638
.argument('<port>', 'Port to serve on')
3739
.argument('<logFile>', 'Log file path')

genkit-tools/common/src/utils/errors.ts

Lines changed: 90 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// genkit-tools/common/src/utils/errors.ts
2-
31
/**
42
* Copyright 2024 Google LLC
53
*
@@ -16,6 +14,26 @@
1614
* limitations under the License.
1715
*/
1816

17+
// Connection error codes for different runtimes
18+
const CONNECTION_ERROR_CODES = {
19+
NODE_ECONNREFUSED: 'ECONNREFUSED',
20+
BUN_CONNECTION_REFUSED: 'ConnectionRefused',
21+
ECONNRESET: 'ECONNRESET',
22+
} as const;
23+
24+
const CONNECTION_ERROR_PATTERNS = [
25+
'ECONNREFUSED',
26+
'Connection refused',
27+
'ConnectionRefused',
28+
'connect ECONNREFUSED',
29+
] as const;
30+
31+
type ErrorWithCode = {
32+
code?: string;
33+
message?: string;
34+
cause?: ErrorWithCode;
35+
};
36+
1937
/**
2038
* Checks if an error is a connection refused error across Node.js and Bun runtimes.
2139
*
@@ -27,58 +45,57 @@ export function isConnectionRefusedError(error: unknown): boolean {
2745
return false;
2846
}
2947

30-
// Handle plain objects with a code property (Bun fetch errors)
31-
if (typeof error === 'object' && 'code' in error) {
32-
const code = (error as any).code;
33-
if (
34-
code === 'ECONNREFUSED' || // Node.js
35-
code === 'ConnectionRefused' || // Bun
36-
code === 'ECONNRESET' // Connection reset (also indicates server is down)
37-
) {
38-
return true;
39-
}
48+
const errorCode = getErrorCode(error);
49+
if (errorCode && isConnectionErrorCode(errorCode)) {
50+
return true;
4051
}
4152

42-
// Handle Error instances
43-
if (error instanceof Error) {
44-
// Direct error code
45-
if ('code' in error && typeof error.code === 'string') {
46-
const code = error.code;
47-
if (
48-
code === 'ECONNREFUSED' ||
49-
code === 'ConnectionRefused' ||
50-
code === 'ECONNRESET'
51-
) {
52-
return true;
53-
}
54-
}
55-
56-
// Node.js style with cause
57-
if (
58-
'cause' in error &&
59-
error.cause &&
60-
typeof error.cause === 'object' &&
61-
'code' in error.cause &&
62-
error.cause.code === 'ECONNREFUSED'
63-
) {
64-
return true;
65-
}
66-
67-
// Fallback: check error message
68-
if (
69-
error.message &&
70-
(error.message.includes('ECONNREFUSED') ||
71-
error.message.includes('Connection refused') ||
72-
error.message.includes('ConnectionRefused') ||
73-
error.message.includes('connect ECONNREFUSED'))
74-
) {
75-
return true;
76-
}
53+
// Fallback: check error message
54+
if (isErrorWithMessage(error)) {
55+
return CONNECTION_ERROR_PATTERNS.some((pattern) =>
56+
error.message.includes(pattern)
57+
);
7758
}
7859

7960
return false;
8061
}
8162

63+
/**
64+
* Helper function to check if a code is a connection error code.
65+
*/
66+
function isConnectionErrorCode(code: string): boolean {
67+
return Object.values(CONNECTION_ERROR_CODES).includes(
68+
code as (typeof CONNECTION_ERROR_CODES)[keyof typeof CONNECTION_ERROR_CODES]
69+
);
70+
}
71+
72+
/**
73+
* Type guard to check if an error has a message property.
74+
*/
75+
function isErrorWithMessage(error: unknown): error is { message: string } {
76+
return (
77+
typeof error === 'object' &&
78+
error !== null &&
79+
'message' in error &&
80+
typeof (error as any).message === 'string'
81+
);
82+
}
83+
84+
/**
85+
* Extracts error code from an object, handling nested structures.
86+
*/
87+
function extractErrorCode(obj: unknown): string | undefined {
88+
if (
89+
typeof obj === 'object' &&
90+
obj !== null &&
91+
'code' in obj &&
92+
typeof (obj as ErrorWithCode).code === 'string'
93+
) {
94+
return (obj as ErrorWithCode).code;
95+
}
96+
return undefined;
97+
}
98+
8299
/**
83100
* Gets the error code from an error object, handling both Node.js and Bun styles.
84101
*/
@@ -87,32 +104,33 @@ export function getErrorCode(error: unknown): string | undefined {
87104
return undefined;
88105
}
89106

90-
// Handle plain objects with a code property
91-
if (
92-
typeof error === 'object' &&
93-
'code' in error &&
94-
typeof (error as any).code === 'string'
95-
) {
96-
return (error as any).code;
107+
// Direct error code
108+
const directCode = extractErrorCode(error);
109+
if (directCode) {
110+
return directCode;
97111
}
98112

99-
// Handle Error instances
100-
if (error instanceof Error) {
101-
// Direct error code
102-
if ('code' in error && typeof error.code === 'string') {
103-
return error.code;
113+
// Node.js style with cause
114+
if (typeof error === 'object' && error !== null && 'cause' in error) {
115+
const causeCode = extractErrorCode((error as ErrorWithCode).cause);
116+
if (causeCode) {
117+
return causeCode;
104118
}
119+
}
105120

106-
// Node.js style with cause
107-
if (
108-
'cause' in error &&
109-
error.cause &&
110-
typeof error.cause === 'object' &&
111-
'code' in error.cause &&
112-
typeof error.cause.code === 'string'
113-
) {
114-
return error.cause.code;
115-
}
121+
return undefined;
122+
}
123+
124+
/**
125+
* Extracts error message from various error formats.
126+
*/
127+
function extractErrorMessage(error: unknown): string | undefined {
128+
if (error instanceof Error) {
129+
return error.message;
130+
}
131+
132+
if (isErrorWithMessage(error)) {
133+
return error.message;
116134
}
117135

118136
return undefined;
@@ -122,18 +140,14 @@ export function getErrorCode(error: unknown): string | undefined {
122140
* Safely extracts error details for logging.
123141
*/
124142
export function getErrorDetails(error: unknown): string {
125-
if (!error) {
143+
if (error === null || error === undefined) {
126144
return 'Unknown error';
127145
}
128146

129147
const code = getErrorCode(error);
148+
const message = extractErrorMessage(error);
130149

131-
if (error instanceof Error) {
132-
return code ? `${error.message} (${code})` : error.message;
133-
}
134-
135-
if (typeof error === 'object' && 'message' in error) {
136-
const message = (error as any).message;
150+
if (message) {
137151
return code ? `${message} (${code})` : message;
138152
}
139153

0 commit comments

Comments
 (0)