diff --git a/package.json b/package.json index 5912f7cc8..f1a5eb274 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "deploy": "npm run setup-kv && wrangler deploy", "dev": "wrangler dev", "start": "wrangler dev", - "test": "vitest", + "test": "vitest run", "setup-kv": "node scripts/setup-kv.cjs", "build:node": "node_modules/.bin/esbuild src/platforms/node-server.js --bundle --platform=node --target=node18 --format=cjs --outfile=dist/node-server.cjs", "dev:node": "npm run build:node && node dist/node-server.cjs" diff --git a/src/builders/ClashConfigBuilder.js b/src/builders/ClashConfigBuilder.js index d4222dbfe..7f520e628 100644 --- a/src/builders/ClashConfigBuilder.js +++ b/src/builders/ClashConfigBuilder.js @@ -6,6 +6,7 @@ import { addProxyWithDedup } from './helpers/proxyHelpers.js'; import { buildSelectorMembers, buildNodeSelectMembers, buildCustomRuleMembers, uniqueNames } from './helpers/groupBuilder.js'; import { emitClashRules, sanitizeClashProxyGroups } from './helpers/clashConfigUtils.js'; import { normalizeGroupName, findGroupIndexByName } from './helpers/groupNameUtils.js'; +import { InvalidConfigError } from '../services/errors.js'; /** * Check if the client supports MRS (Meta Rule Set) format @@ -600,11 +601,7 @@ export class ClashConfigBuilder extends BaseConfigBuilder { newGroup.use = newGroup.use.filter(p => allProviderNames.has(p)); } - // Add group if: - // 1. Has valid proxies or use, OR - // 2. Is url-test/fallback type (will be filled by validateProxyGroups) - const isAutoFillableType = newGroup.type === 'url-test' || newGroup.type === 'fallback'; - if ((newGroup.proxies?.length > 0) || (newGroup.use?.length > 0) || isAutoFillableType) { + if ((newGroup.proxies?.length > 0) || (newGroup.use?.length > 0) || newGroup.type) { this.config['proxy-groups'].push(newGroup); } } @@ -612,25 +609,27 @@ export class ClashConfigBuilder extends BaseConfigBuilder { } /** - * Validate proxy groups before final output - * Ensures url-test/fallback groups have proxies, fills empty ones with all nodes + * Reject invalid proxy groups before final output. + * Why: empty groups make Clash reject the whole config, so we should fail fast + * instead of masking the upstream merge/parsing problem. */ validateProxyGroups() { - const proxyList = this.getProxyList(); - const providerNames = this.getAllProviderNames(); - (this.config['proxy-groups'] || []).forEach(group => { - // For url-test/fallback groups, ensure they have proxies or providers - if ((group.type === 'url-test' || group.type === 'fallback') && - (!group.proxies || group.proxies.length === 0) && - (!group.use || group.use.length === 0)) { - // Fill with all available proxies - group.proxies = [...proxyList]; - // Also use all providers if available - if (providerNames.length > 0) { - group.use = [...providerNames]; - } + const requiresMembers = group?.type === 'url-test' || group?.type === 'fallback'; + if (!requiresMembers) { + return; + } + + const hasProxyRefs = Array.isArray(group.proxies) && group.proxies.length > 0; + const hasProviderRefs = Array.isArray(group.use) && group.use.length > 0; + if (hasProxyRefs || hasProviderRefs) { + return; } + + const groupName = group?.name || '(unnamed group)'; + throw new InvalidConfigError( + `Invalid proxy group "${groupName}": type "${group.type}" requires at least one proxy or provider reference` + ); }); } @@ -657,10 +656,8 @@ export class ClashConfigBuilder extends BaseConfigBuilder { }; } - // Validate proxy groups: fill empty url-test/fallback groups with all proxies - this.validateProxyGroups(); - sanitizeClashProxyGroups(this.config); + this.validateProxyGroups(); this.config.rules = [ ...ruleResults, diff --git a/src/services/errors.js b/src/services/errors.js index 8288603a0..1b27c692e 100644 --- a/src/services/errors.js +++ b/src/services/errors.js @@ -19,3 +19,10 @@ export class InvalidPayloadError extends ServiceError { this.name = 'InvalidPayloadError'; } } + +export class InvalidConfigError extends ServiceError { + constructor(message = 'Invalid config') { + super(message, 400); + this.name = 'InvalidConfigError'; + } +} diff --git a/test/proxy-groups-override.test.js b/test/proxy-groups-override.test.js index d90d3bfa0..3a3eafc02 100644 --- a/test/proxy-groups-override.test.js +++ b/test/proxy-groups-override.test.js @@ -300,7 +300,7 @@ proxy-groups: expect(autoGroups[0].url).toBe('http://custom.test/204'); }); - it('should fill empty url-test proxies with all available nodes', async () => { + it('should reject empty url-test groups instead of silently filling them', async () => { const inputWithEmptyProxies = ` proxies: - name: Node-A @@ -321,15 +321,9 @@ proxy-groups: proxies: [] `; const builder = new ClashConfigBuilder(inputWithEmptyProxies, 'minimal', [], null, 'zh-CN', 'test-agent'); - const yamlText = await builder.build(); - const config = yaml.load(yamlText); - - const emptyGroup = config['proxy-groups'].find(g => g.name === 'Empty Test Group'); - expect(emptyGroup).toBeDefined(); - // Empty group should be filled with all available proxies - expect(emptyGroup.proxies.length).toBeGreaterThan(0); - expect(emptyGroup.proxies).toContain('Node-A'); - expect(emptyGroup.proxies).toContain('Node-B'); + await expect(builder.build()).rejects.toThrow( + 'Invalid proxy group "Empty Test Group": type "url-test" requires at least one proxy or provider reference' + ); }); it('should filter out invalid proxy references from user groups', async () => { diff --git a/test/worker.test.js b/test/worker.test.js index 5f097b20c..e90ce6f35 100644 --- a/test/worker.test.js +++ b/test/worker.test.js @@ -77,6 +77,29 @@ describe('Worker', () => { expect(text).toContain('proxies:'); }); + it('GET /clash rejects empty url-test proxy groups with a diagnostic error', async () => { + const app = createTestApp(); + const config = ` +proxies: + - name: Node-A + type: ss + server: a.example.com + port: 443 + cipher: aes-128-gcm + password: test +proxy-groups: + - name: Empty Test Group + type: url-test + proxies: [] +`; + const res = await app.request(`http://localhost/clash?config=${encodeURIComponent(config)}`); + + expect(res.status).toBe(400); + const text = await res.text(); + expect(text).toContain('Invalid proxy group "Empty Test Group"'); + expect(text).toContain('requires at least one proxy or provider reference'); + }); + it('GET /shorten-v2 returns short code', async () => { const url = 'http://example.com'; const kvMock = {