mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-17 03:57:18 +00:00
* fix(cli): stop double-wrapping and double-printing API errors in non-interactive mode In non-interactive (-p) mode, any upstream 4xx ended up on stderr three times: once from the stream-error handler, once from handleError after the thrown Error.message (already containing the formatted text) was fed back through parseAndFormatApiError producing "[API Error: [API Error: ...]]", and once more from JsonOutputAdapter.emitResult writing the same errorMessage out in TEXT mode. The top-level catch then framed the resulting throw as "An unexpected critical error occurred:" with a stack trace, which made a routine 4xx look like a CLI crash. Three coordinated changes: - AlreadyReportedError marks a throw whose message is already on the wire. handleError short-circuits on it: no second writeStderrLine, no second parseAndFormatApiError, just propagate the exit code. - The non-interactive stream-error handler now throws AlreadyReportedError, and the catch block skips the adapter's emitResult in TEXT mode so we don't get a third copy. - The top-level .catch in packages/cli/index.ts treats AlreadyReportedError as a routine, already-reported failure: exit with the carried code without printing the "unexpected critical" framing or the stack trace. parseAndFormatApiError is also made idempotent — input that already starts with "[API Error: " and ends with "]" is returned unchanged. That is the safety net: even if a future caller forgets to mark its throw, the double-wrap symptom is impossible. Tests cover all three layers: idempotency in errorParsing, the short-circuit in handleError, and a regression test on runNonInteractive that asserts no "[API Error: [API Error: ...]" line is ever produced on stderr. Fixes #3748 * fix(cli): make API error formatting idempotent for 429 suffix + use AlreadyReportedError * fix(cli): follow up on error reporting review
112 lines
3.2 KiB
JavaScript
112 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { initStartupProfiler } from './src/utils/startupProfiler.js';
|
|
|
|
// Must run before any other imports to capture the earliest possible T0.
|
|
initStartupProfiler();
|
|
|
|
import './src/gemini.js';
|
|
import { main } from './src/gemini.js';
|
|
import { FatalError } from '@qwen-code/qwen-code-core';
|
|
import { AlreadyReportedError } from './src/utils/errors.js';
|
|
import { writeStderrLine } from './src/utils/stdioHelpers.js';
|
|
|
|
// --- Global Entry Point ---
|
|
|
|
// Suppress known race conditions in @lydell/node-pty.
|
|
//
|
|
// PTY errors that are expected due to timing races between process exit
|
|
// and I/O operations. These should not crash the app.
|
|
//
|
|
// References:
|
|
// - https://github.com/microsoft/node-pty/issues/178 (EIO on macOS/Linux)
|
|
// - https://github.com/microsoft/node-pty/issues/827 (resize on Windows)
|
|
const getErrnoCode = (error: unknown): string | undefined => {
|
|
if (!error || typeof error !== 'object') {
|
|
return undefined;
|
|
}
|
|
const code = (error as { code?: unknown }).code;
|
|
return typeof code === 'string' ? code : undefined;
|
|
};
|
|
|
|
const isExpectedPtyRaceError = (error: unknown): boolean => {
|
|
if (!(error instanceof Error)) {
|
|
return false;
|
|
}
|
|
|
|
const message = error.message;
|
|
const code = getErrnoCode(error);
|
|
|
|
// EIO: PTY read race on macOS/Linux - code + PTY context required
|
|
// https://github.com/microsoft/node-pty/issues/178
|
|
if (
|
|
(code === 'EIO' && message.includes('read')) ||
|
|
message.includes('read EIO')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// EAGAIN: transient non-blocking read error from PTY fd
|
|
if (
|
|
(code === 'EAGAIN' && message.includes('read')) ||
|
|
message.includes('read EAGAIN')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// PTY-specific resize/exit race errors - require PTY context in message
|
|
if (
|
|
message.includes('ioctl(2) failed, EBADF') ||
|
|
message.includes('Cannot resize a pty that has already exited')
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
if (isExpectedPtyRaceError(error)) {
|
|
return;
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
writeStderrLine(error.stack ?? error.message);
|
|
} else {
|
|
writeStderrLine(String(error));
|
|
}
|
|
process.exit(1);
|
|
});
|
|
|
|
main().catch((error) => {
|
|
if (error instanceof FatalError) {
|
|
let errorMessage = error.message;
|
|
if (!process.env['NO_COLOR']) {
|
|
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
|
}
|
|
console.error(errorMessage);
|
|
process.exit(error.exitCode);
|
|
}
|
|
// AlreadyReportedError means an upstream layer (e.g. the non-interactive
|
|
// stream-error handler) has already written the user-facing message to
|
|
// stderr and just wants to surface a non-zero exit code. Don't print
|
|
// "An unexpected critical error occurred:" with a stack trace — that
|
|
// framing is for genuinely unexpected, programmer-level bugs, and a
|
|
// routine 4xx from an upstream API does not qualify.
|
|
if (error instanceof AlreadyReportedError) {
|
|
process.exit(error.exitCode);
|
|
}
|
|
console.error('An unexpected critical error occurred:');
|
|
if (error instanceof Error) {
|
|
console.error(error.stack);
|
|
} else {
|
|
console.error(String(error));
|
|
}
|
|
process.exit(1);
|
|
});
|