mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 01:23:53 +00:00
Standardize exit codes (#7055)
This commit is contained in:
committed by
GitHub
parent
415a36a195
commit
7e31577813
@@ -74,6 +74,18 @@ This guide provides solutions to common issues and debugging tips, including top
|
||||
- **Cause:** The `DEBUG` and `DEBUG_MODE` variables are automatically excluded from project `.env` files to prevent interference with gemini-cli behavior.
|
||||
- **Solution:** Use a `.gemini/.env` file instead, or configure the `excludedProjectEnvVars` setting in your `settings.json` to exclude fewer variables.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
The Gemini CLI uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation.
|
||||
|
||||
| Exit Code | Error Type | Description |
|
||||
| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. |
|
||||
| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) |
|
||||
| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g., Docker, Podman, or Seatbelt). |
|
||||
| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. |
|
||||
| 53 | `FatalTurnLimitedError` | The maximum number of conversational turns for the session was reached. (non-interactive mode only) |
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
- **CLI debugging:**
|
||||
|
||||
@@ -8,9 +8,18 @@
|
||||
|
||||
import './src/gemini.js';
|
||||
import { main } from './src/gemini.js';
|
||||
import { FatalError } from '@google/gemini-cli-core';
|
||||
|
||||
// --- Global Entry Point ---
|
||||
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);
|
||||
}
|
||||
console.error('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
console.error(error.stack);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { SandboxConfig } from '@google/gemini-cli-core';
|
||||
import { FatalSandboxError } from '@google/gemini-cli-core';
|
||||
import commandExists from 'command-exists';
|
||||
import * as os from 'node:os';
|
||||
import { getPackageJson } from '../utils/package.js';
|
||||
@@ -51,21 +52,19 @@ function getSandboxCommand(
|
||||
|
||||
if (typeof sandbox === 'string' && sandbox) {
|
||||
if (!isSandboxCommand(sandbox)) {
|
||||
console.error(
|
||||
`ERROR: invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
|
||||
throw new FatalSandboxError(
|
||||
`Invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// confirm that specified command exists
|
||||
if (commandExists.sync(sandbox)) {
|
||||
return sandbox;
|
||||
}
|
||||
console.error(
|
||||
`ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
||||
throw new FatalSandboxError(
|
||||
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// look for seatbelt, docker, or podman, in that order
|
||||
@@ -80,11 +79,10 @@ function getSandboxCommand(
|
||||
|
||||
// throw an error if user requested sandbox but no command was found
|
||||
if (sandbox === true) {
|
||||
console.error(
|
||||
'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
|
||||
throw new FatalSandboxError(
|
||||
'GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
|
||||
'install docker or podman or specify command in GEMINI_SANDBOX',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
main,
|
||||
@@ -16,6 +15,7 @@ import type { SettingsFile } from './config/settings.js';
|
||||
import { LoadedSettings, loadSettings } from './config/settings.js';
|
||||
import { appEvents, AppEvent } from './utils/events.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { FatalConfigError } from '@google/gemini-cli-core';
|
||||
|
||||
// Custom error to identify mock process.exit calls
|
||||
class MockProcessExitError extends Error {
|
||||
@@ -75,7 +75,6 @@ vi.mock('./utils/sandbox.js', () => ({
|
||||
}));
|
||||
|
||||
describe('gemini.tsx main function', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let loadSettingsMock: ReturnType<typeof vi.mocked<typeof loadSettings>>;
|
||||
let originalEnvGeminiSandbox: string | undefined;
|
||||
let originalEnvSandbox: string | undefined;
|
||||
@@ -97,7 +96,6 @@ describe('gemini.tsx main function', () => {
|
||||
delete process.env['GEMINI_SANDBOX'];
|
||||
delete process.env['SANDBOX'];
|
||||
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
initialUnhandledRejectionListeners =
|
||||
process.listeners('unhandledRejection');
|
||||
});
|
||||
@@ -126,7 +124,7 @@ describe('gemini.tsx main function', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call process.exit(1) if settings have errors', async () => {
|
||||
it('should throw InvalidConfigurationError if settings have errors', async () => {
|
||||
const settingsError = {
|
||||
message: 'Test settings error',
|
||||
path: '/test/settings.json',
|
||||
@@ -158,28 +156,7 @@ describe('gemini.tsx main function', () => {
|
||||
|
||||
loadSettingsMock.mockReturnValue(mockLoadedSettings);
|
||||
|
||||
try {
|
||||
await main();
|
||||
// If main completes without throwing, the test should fail because process.exit was expected
|
||||
expect.fail('main function did not exit as expected');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(MockProcessExitError);
|
||||
if (error instanceof MockProcessExitError) {
|
||||
expect(error.code).toBe(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify console.error was called with the error message
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
||||
expect(stripAnsi(String(consoleErrorSpy.mock.calls[0][0]))).toBe(
|
||||
'Error in /test/settings.json: Test settings error',
|
||||
);
|
||||
expect(stripAnsi(String(consoleErrorSpy.mock.calls[1][0]))).toBe(
|
||||
'Please fix /test/settings.json and try again.',
|
||||
);
|
||||
|
||||
// Verify process.exit was called.
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
await expect(main()).rejects.toThrow(FatalConfigError);
|
||||
});
|
||||
|
||||
it('should log unhandled promise rejections and open debug console on first error', async () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
logIdeConnection,
|
||||
IdeConnectionEvent,
|
||||
IdeConnectionType,
|
||||
FatalConfigError,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
|
||||
@@ -173,15 +174,12 @@ export async function main() {
|
||||
|
||||
await cleanupCheckpoints();
|
||||
if (settings.errors.length > 0) {
|
||||
for (const error of settings.errors) {
|
||||
let errorMessage = `Error in ${error.path}: ${error.message}`;
|
||||
if (!process.env['NO_COLOR']) {
|
||||
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
|
||||
}
|
||||
console.error(errorMessage);
|
||||
console.error(`Please fix ${error.path} and try again.`);
|
||||
}
|
||||
process.exit(1);
|
||||
const errorMessages = settings.errors.map(
|
||||
(error) => `Error in ${error.path}: ${error.message}`,
|
||||
);
|
||||
throw new FatalConfigError(
|
||||
`${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`,
|
||||
);
|
||||
}
|
||||
|
||||
const argv = await parseArguments(settings.merged);
|
||||
|
||||
@@ -38,7 +38,6 @@ describe('runNonInteractive', () => {
|
||||
let mockCoreExecuteToolCall: vi.Mock;
|
||||
let mockShutdownTelemetry: vi.Mock;
|
||||
let consoleErrorSpy: vi.SpyInstance;
|
||||
let processExitSpy: vi.SpyInstance;
|
||||
let processStdoutSpy: vi.SpyInstance;
|
||||
let mockGeminiClient: {
|
||||
sendMessageStream: vi.Mock;
|
||||
@@ -49,9 +48,6 @@ describe('runNonInteractive', () => {
|
||||
mockShutdownTelemetry = vi.mocked(shutdownTelemetry);
|
||||
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => {}) as (code?: number) => never);
|
||||
processStdoutSpy = vi
|
||||
.spyOn(process.stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
@@ -202,7 +198,6 @@ describe('runNonInteractive', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool errorTool: Execution failed',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
|
||||
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
@@ -228,12 +223,9 @@ describe('runNonInteractive', () => {
|
||||
throw apiError;
|
||||
});
|
||||
|
||||
await runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'[API Error: API connection failed]',
|
||||
);
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
await expect(
|
||||
runNonInteractive(mockConfig, 'Initial fail', 'prompt-id-4'),
|
||||
).rejects.toThrow(apiError);
|
||||
});
|
||||
|
||||
it('should not exit if a tool is not found, and should send error back to model', async () => {
|
||||
@@ -272,7 +264,6 @@ describe('runNonInteractive', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error executing tool nonexistentTool: Tool "nonexistentTool" not found in registry.',
|
||||
);
|
||||
expect(processExitSpy).not.toHaveBeenCalled();
|
||||
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||
"Sorry, I can't find that tool.",
|
||||
@@ -281,9 +272,10 @@ describe('runNonInteractive', () => {
|
||||
|
||||
it('should exit when max session turns are exceeded', async () => {
|
||||
vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0);
|
||||
await runNonInteractive(mockConfig, 'Trigger loop', 'prompt-id-6');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
|
||||
await expect(
|
||||
runNonInteractive(mockConfig, 'Trigger loop', 'prompt-id-6'),
|
||||
).rejects.toThrow(
|
||||
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
isTelemetrySdkInitialized,
|
||||
GeminiEventType,
|
||||
parseAndFormatApiError,
|
||||
FatalInputError,
|
||||
FatalTurnLimitedError,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Content, Part } from '@google/genai';
|
||||
|
||||
@@ -53,8 +55,9 @@ export async function runNonInteractive(
|
||||
if (!shouldProceed || !processedQuery) {
|
||||
// An error occurred during @include processing (e.g., file not found).
|
||||
// The error message is already logged by handleAtCommand.
|
||||
console.error('Exiting due to an error processing the @ command.');
|
||||
process.exit(1);
|
||||
throw new FatalInputError(
|
||||
'Exiting due to an error processing the @ command.',
|
||||
);
|
||||
}
|
||||
|
||||
let currentMessages: Content[] = [
|
||||
@@ -68,10 +71,9 @@ export async function runNonInteractive(
|
||||
config.getMaxSessionTurns() >= 0 &&
|
||||
turnCount > config.getMaxSessionTurns()
|
||||
) {
|
||||
console.error(
|
||||
'\n Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
|
||||
throw new FatalTurnLimitedError(
|
||||
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const toolCallRequests: ToolCallRequestInfo[] = [];
|
||||
|
||||
@@ -126,7 +128,7 @@ export async function runNonInteractive(
|
||||
config.getContentGeneratorConfig()?.authType,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
throw error;
|
||||
} finally {
|
||||
consolePatcher.cleanup();
|
||||
if (isTelemetrySdkInitialized()) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../config/settings.js';
|
||||
import { promisify } from 'node:util';
|
||||
import type { Config, SandboxConfig } from '@google/gemini-cli-core';
|
||||
import { FatalSandboxError } from '@google/gemini-cli-core';
|
||||
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -198,8 +199,9 @@ export async function start_sandbox(
|
||||
if (config.command === 'sandbox-exec') {
|
||||
// disallow BUILD_SANDBOX
|
||||
if (process.env['BUILD_SANDBOX']) {
|
||||
console.error('ERROR: cannot BUILD_SANDBOX when using macOS Seatbelt');
|
||||
process.exit(1);
|
||||
throw new FatalSandboxError(
|
||||
'Cannot BUILD_SANDBOX when using macOS Seatbelt',
|
||||
);
|
||||
}
|
||||
|
||||
const profile = (process.env['SEATBELT_PROFILE'] ??= 'permissive-open');
|
||||
@@ -214,10 +216,9 @@ export async function start_sandbox(
|
||||
);
|
||||
}
|
||||
if (!fs.existsSync(profileFile)) {
|
||||
console.error(
|
||||
`ERROR: missing macos seatbelt profile file '${profileFile}'`,
|
||||
throw new FatalSandboxError(
|
||||
`Missing macos seatbelt profile file '${profileFile}'`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// Log on STDERR so it doesn't clutter the output on STDOUT
|
||||
console.error(`using macos seatbelt (profile: ${profile}) ...`);
|
||||
@@ -325,13 +326,12 @@ export async function start_sandbox(
|
||||
console.error(data.toString());
|
||||
});
|
||||
proxyProcess.on('close', (code, signal) => {
|
||||
console.error(
|
||||
`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
if (sandboxProcess?.pid) {
|
||||
process.kill(-sandboxProcess.pid, 'SIGTERM');
|
||||
}
|
||||
process.exit(1);
|
||||
throw new FatalSandboxError(
|
||||
`Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
});
|
||||
console.log('waiting for proxy to start ...');
|
||||
await execAsync(
|
||||
@@ -366,11 +366,10 @@ export async function start_sandbox(
|
||||
// note this can only be done with binary linked from gemini-cli repo
|
||||
if (process.env['BUILD_SANDBOX']) {
|
||||
if (!gcPath.includes('gemini-cli/packages/')) {
|
||||
console.error(
|
||||
'ERROR: cannot build sandbox using installed gemini binary; ' +
|
||||
throw new FatalSandboxError(
|
||||
'Cannot build sandbox using installed gemini binary; ' +
|
||||
'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.',
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.error('building sandbox ...');
|
||||
const gcRoot = gcPath.split('/packages/')[0];
|
||||
@@ -403,10 +402,9 @@ export async function start_sandbox(
|
||||
image === LOCAL_DEV_SANDBOX_IMAGE_NAME
|
||||
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
|
||||
: 'Please check the image name, your network connection, or notify gemini-cli-dev@google.com if the issue persists.';
|
||||
console.error(
|
||||
`ERROR: Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
|
||||
throw new FatalSandboxError(
|
||||
`Sandbox image '${image}' is missing or could not be pulled. ${remedy}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// use interactive mode and auto-remove container on exit
|
||||
@@ -484,17 +482,15 @@ export async function start_sandbox(
|
||||
mount = `${from}:${to}:${opts}`;
|
||||
// check that from path is absolute
|
||||
if (!path.isAbsolute(from)) {
|
||||
console.error(
|
||||
`ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
|
||||
throw new FatalSandboxError(
|
||||
`Path '${from}' listed in SANDBOX_MOUNTS must be absolute`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// check that from path exists on host
|
||||
if (!fs.existsSync(from)) {
|
||||
console.error(
|
||||
`ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`,
|
||||
throw new FatalSandboxError(
|
||||
`Missing mount path '${from}' listed in SANDBOX_MOUNTS`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
|
||||
args.push('--volume', mount);
|
||||
@@ -665,10 +661,9 @@ export async function start_sandbox(
|
||||
console.error(`SANDBOX_ENV: ${env}`);
|
||||
args.push('--env', env);
|
||||
} else {
|
||||
console.error(
|
||||
'ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs',
|
||||
throw new FatalSandboxError(
|
||||
'SANDBOX_ENV must be a comma-separated list of key=value pairs',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -776,13 +771,12 @@ export async function start_sandbox(
|
||||
console.error(data.toString().trim());
|
||||
});
|
||||
proxyProcess.on('close', (code, signal) => {
|
||||
console.error(
|
||||
`ERROR: proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
if (sandboxProcess?.pid) {
|
||||
process.kill(-sandboxProcess.pid, 'SIGTERM');
|
||||
}
|
||||
process.exit(1);
|
||||
throw new FatalSandboxError(
|
||||
`Proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`,
|
||||
);
|
||||
});
|
||||
console.log('waiting for proxy to start ...');
|
||||
await execAsync(
|
||||
|
||||
@@ -18,7 +18,7 @@ import open from 'open';
|
||||
import path from 'node:path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import { getErrorMessage, FatalAuthenticationError } from '../utils/errors.js';
|
||||
import { UserAccountManager } from '../utils/userAccountManager.js';
|
||||
import { AuthType } from '../core/contentGenerator.js';
|
||||
import readline from 'node:readline';
|
||||
@@ -142,7 +142,9 @@ async function initOauthClient(
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
process.exit(1);
|
||||
throw new FatalAuthenticationError(
|
||||
'Failed to authenticate with user code.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const webLogin = await authWithWeb(client);
|
||||
@@ -166,7 +168,7 @@ async function initOauthClient(
|
||||
console.error(
|
||||
'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.',
|
||||
);
|
||||
process.exit(1);
|
||||
throw new FatalAuthenticationError('Failed to open browser.');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
@@ -174,7 +176,7 @@ async function initOauthClient(
|
||||
err,
|
||||
'\nPlease try running again with NO_BROWSER=true set.',
|
||||
);
|
||||
process.exit(1);
|
||||
throw new FatalAuthenticationError('Failed to open browser.');
|
||||
}
|
||||
console.log('Waiting for authentication...');
|
||||
|
||||
|
||||
@@ -25,6 +25,41 @@ export function getErrorMessage(error: unknown): string {
|
||||
}
|
||||
}
|
||||
|
||||
export class FatalError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly exitCode: number,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class FatalAuthenticationError extends FatalError {
|
||||
constructor(message: string) {
|
||||
super(message, 41);
|
||||
}
|
||||
}
|
||||
export class FatalInputError extends FatalError {
|
||||
constructor(message: string) {
|
||||
super(message, 42);
|
||||
}
|
||||
}
|
||||
export class FatalSandboxError extends FatalError {
|
||||
constructor(message: string) {
|
||||
super(message, 44);
|
||||
}
|
||||
}
|
||||
export class FatalConfigError extends FatalError {
|
||||
constructor(message: string) {
|
||||
super(message, 52);
|
||||
}
|
||||
}
|
||||
export class FatalTurnLimitedError extends FatalError {
|
||||
constructor(message: string) {
|
||||
super(message, 53);
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends Error {}
|
||||
export class UnauthorizedError extends Error {}
|
||||
export class BadRequestError extends Error {}
|
||||
|
||||
Reference in New Issue
Block a user