🛠️ feat: Add Config Validation Bypass & Improve Error Handling (#10733)

* fix: exit on invalid configuration and add bypass validation warning

* test: enhance loadCustomConfig tests for exit behavior on invalid config
This commit is contained in:
Marco Beretta 2025-12-03 23:23:36 +01:00 committed by Danny Avila
parent 7080c61525
commit 6aa696b74f
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
2 changed files with 54 additions and 13 deletions

View file

@ -85,26 +85,32 @@ Please specify a correct \`imageOutputType\` value (case-sensitive).
let errorMessage = `Invalid custom config file at ${configPath}: let errorMessage = `Invalid custom config file at ${configPath}:
${JSON.stringify(result.error, null, 2)}`; ${JSON.stringify(result.error, null, 2)}`;
if (i === 0) { logger.error(errorMessage);
logger.error(errorMessage); const speechError = result.error.errors.find(
const speechError = result.error.errors.find( (err) =>
(err) => err.code === 'unrecognized_keys' &&
err.code === 'unrecognized_keys' && (err.message?.includes('stt') || err.message?.includes('tts')),
(err.message?.includes('stt') || err.message?.includes('tts')), );
);
if (speechError) { if (speechError) {
logger.warn(` logger.warn(`
The Speech-to-text and Text-to-speech configuration format has recently changed. The Speech-to-text and Text-to-speech configuration format has recently changed.
If you're getting this error, please refer to the latest documentation: If you're getting this error, please refer to the latest documentation:
https://www.librechat.ai/docs/configuration/stt_tts`); https://www.librechat.ai/docs/configuration/stt_tts`);
}
i++;
} }
return null; if (process.env.CONFIG_BYPASS_VALIDATION === 'true') {
logger.warn(
'CONFIG_BYPASS_VALIDATION is enabled. Continuing with default configuration despite validation errors.',
);
return null;
}
logger.error(
'Exiting due to invalid configuration. Set CONFIG_BYPASS_VALIDATION=true to bypass this check.',
);
process.exit(1);
} else { } else {
if (printConfig) { if (printConfig) {
logger.info('Custom config file loaded:'); logger.info('Custom config file loaded:');

View file

@ -50,8 +50,25 @@ const { logger } = require('@librechat/data-schemas');
const loadCustomConfig = require('./loadCustomConfig'); const loadCustomConfig = require('./loadCustomConfig');
describe('loadCustomConfig', () => { describe('loadCustomConfig', () => {
const originalExit = process.exit;
const mockExit = jest.fn((code) => {
throw new Error(`process.exit called with "${code}"`);
});
beforeAll(() => {
process.exit = mockExit;
});
afterAll(() => {
process.exit = originalExit;
});
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
// Re-apply the exit mock implementation after resetAllMocks
mockExit.mockImplementation((code) => {
throw new Error(`process.exit called with "${code}"`);
});
delete process.env.CONFIG_PATH; delete process.env.CONFIG_PATH;
}); });
@ -94,20 +111,38 @@ describe('loadCustomConfig', () => {
it('should return null and log if config schema validation fails', async () => { it('should return null and log if config schema validation fails', async () => {
const invalidConfig = { invalidField: true }; const invalidConfig = { invalidField: true };
process.env.CONFIG_PATH = 'invalidConfig.yaml'; process.env.CONFIG_PATH = 'invalidConfig.yaml';
process.env.CONFIG_BYPASS_VALIDATION = 'true';
loadYaml.mockReturnValueOnce(invalidConfig); loadYaml.mockReturnValueOnce(invalidConfig);
const result = await loadCustomConfig(); const result = await loadCustomConfig();
expect(result).toBeNull(); expect(result).toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
'CONFIG_BYPASS_VALIDATION is enabled. Continuing with default configuration despite validation errors.',
);
delete process.env.CONFIG_BYPASS_VALIDATION;
});
it('should call process.exit(1) when config validation fails without bypass', async () => {
const invalidConfig = { invalidField: true };
process.env.CONFIG_PATH = 'invalidConfig.yaml';
loadYaml.mockReturnValueOnce(invalidConfig);
await expect(loadCustomConfig()).rejects.toThrow('process.exit called with "1"');
expect(logger.error).toHaveBeenCalledWith(
'Exiting due to invalid configuration. Set CONFIG_BYPASS_VALIDATION=true to bypass this check.',
);
}); });
it('should handle and return null on YAML parse error for a string response from remote', async () => { it('should handle and return null on YAML parse error for a string response from remote', async () => {
process.env.CONFIG_PATH = 'http://example.com/config.yaml'; process.env.CONFIG_PATH = 'http://example.com/config.yaml';
process.env.CONFIG_BYPASS_VALIDATION = 'true';
axios.get.mockResolvedValue({ data: 'invalidYAMLContent' }); axios.get.mockResolvedValue({ data: 'invalidYAMLContent' });
const result = await loadCustomConfig(); const result = await loadCustomConfig();
expect(result).toBeNull(); expect(result).toBeNull();
delete process.env.CONFIG_BYPASS_VALIDATION;
}); });
it('should return the custom config object for a valid remote config file', async () => { it('should return the custom config object for a valid remote config file', async () => {