Skip to content

Commit 67a6b53

Browse files
committed
Show URLRules and NoBackendRetryAfter in UI
- Include URLRules and Proxy.NoBackendRetryAfter in the config view exposed via /rpc/ (internal/app/config_view.go, internal/dashboard/config.go) - Regenerate ProxySection / RateLimitSection wiring in internal/dashboard/dashboard_zenrpc.go for the new fields - Render NoBackendRetryAfter under Proxy in the dashboard config tab and add a static line describing proxy error codes (cb_open=503+RA, upstream_error=502, no_backends=503+RA, panic=500) - Add URLRules block (Path/Method/Host inputs, AND-semantics) and NoBackendRetryAfter input under Circuit Breaker in the config builder, with TOML round-tripping (default config, importer, emitter)
1 parent 7c0e49c commit 67a6b53

9 files changed

Lines changed: 220 additions & 46 deletions

File tree

internal/app/config_view.go

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,22 @@ func buildProxySection(c Config) dashboard.ProxySection {
2525
td := c.Proxy.TargetDiscovery
2626

2727
return dashboard.ProxySection{
28-
Listen: c.Proxy.Listen,
29-
Targets: c.Proxy.Targets,
30-
ServiceName: c.Proxy.ServiceName,
31-
Platforms: c.Proxy.Platforms,
32-
ReadTimeout: c.Proxy.Timeouts.Read,
33-
WriteTimeout: c.Proxy.Timeouts.Write,
34-
IdleTimeout: c.Proxy.Timeouts.Idle,
35-
MaxRequestBody: c.Proxy.Limits.MaxRequestBody,
36-
RealIPHeaders: c.Proxy.RealIP.Headers,
37-
TrustedProxies: c.Proxy.RealIP.TrustedProxies,
38-
CBEnabled: c.Proxy.CircuitBreaker.Enabled == nil || *c.Proxy.CircuitBreaker.Enabled,
39-
CBThreshold: c.Proxy.CircuitBreaker.Threshold,
40-
CBTimeout: c.Proxy.CircuitBreaker.Timeout,
41-
StaticPaths: c.Proxy.Static.Paths,
42-
StaticExts: c.Proxy.Static.Extensions,
28+
Listen: c.Proxy.Listen,
29+
Targets: c.Proxy.Targets,
30+
ServiceName: c.Proxy.ServiceName,
31+
Platforms: c.Proxy.Platforms,
32+
NoBackendRetryAfter: c.Proxy.NoBackendRetryAfter,
33+
ReadTimeout: c.Proxy.Timeouts.Read,
34+
WriteTimeout: c.Proxy.Timeouts.Write,
35+
IdleTimeout: c.Proxy.Timeouts.Idle,
36+
MaxRequestBody: c.Proxy.Limits.MaxRequestBody,
37+
RealIPHeaders: c.Proxy.RealIP.Headers,
38+
TrustedProxies: c.Proxy.RealIP.TrustedProxies,
39+
CBEnabled: c.Proxy.CircuitBreaker.Enabled == nil || *c.Proxy.CircuitBreaker.Enabled,
40+
CBThreshold: c.Proxy.CircuitBreaker.Threshold,
41+
CBTimeout: c.Proxy.CircuitBreaker.Timeout,
42+
StaticPaths: c.Proxy.Static.Paths,
43+
StaticExts: c.Proxy.Static.Extensions,
4344

4445
TargetDiscoveryEnabled: td.TargetDiscoveryEnabled(),
4546
TargetDiscoveryHost: td.Hostname,
@@ -56,7 +57,7 @@ func buildJSONRPCSection(c Config) dashboard.JSONRPCSection {
5657
eps[i] = dashboard.JSONRPCEndpointInfo{
5758
Path: ep.Path, Name: ep.Name,
5859
SchemaURL: ep.SchemaURL, SchemaRefresh: ep.SchemaRefresh,
59-
MethodWhitelist: ep.MethodWhitelist, MaxBatchSize: ep.MaxBatchSize,
60+
MethodWhitelist: ep.MethodWhitelist, MaxBatchSize: ep.MaxBatchSize, MCPMode: ep.MCPMode,
6061
}
6162
}
6263

@@ -72,9 +73,22 @@ func buildRateLimitSection(c Config) dashboard.RateLimitSection {
7273
}
7374
}
7475

76+
urlRules := make([]dashboard.URLRuleInfo, len(c.RateLimit.URLRules))
77+
for i, r := range c.RateLimit.URLRules {
78+
urlRules[i] = dashboard.URLRuleInfo{
79+
Name: r.Name,
80+
Path: r.Path,
81+
Method: r.Method,
82+
Host: r.Host,
83+
Limit: r.Limit,
84+
Action: r.Action,
85+
}
86+
}
87+
7588
return dashboard.RateLimitSection{
7689
Enabled: c.RateLimit.RateLimitEnabled(), PerIP: c.RateLimit.PerIP,
77-
Action: c.RateLimit.Action, MaxCounters: c.RateLimit.MaxCounters, Rules: rules,
90+
Action: c.RateLimit.Action, MaxCounters: c.RateLimit.MaxCounters,
91+
Rules: rules, URLRules: urlRules,
7892
}
7993
}
8094

internal/dashboard/config.go

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,22 @@ type ConfigResponse struct {
4242
}
4343

4444
type ProxySection struct {
45-
Listen string `json:"listen"`
46-
Targets []string `json:"targets"`
47-
ServiceName string `json:"serviceName"`
48-
Platforms []string `json:"platforms,omitempty"`
49-
ReadTimeout string `json:"readTimeout"`
50-
WriteTimeout string `json:"writeTimeout"`
51-
IdleTimeout string `json:"idleTimeout"`
52-
MaxRequestBody string `json:"maxRequestBody"`
53-
RealIPHeaders []string `json:"realIpHeaders"`
54-
TrustedProxies []string `json:"trustedProxies"`
55-
CBEnabled bool `json:"cbEnabled"`
56-
CBThreshold int `json:"cbThreshold"`
57-
CBTimeout string `json:"cbTimeout"`
58-
StaticPaths []string `json:"staticPaths,omitempty"`
59-
StaticExts []string `json:"staticExts,omitempty"`
45+
Listen string `json:"listen"`
46+
Targets []string `json:"targets"`
47+
ServiceName string `json:"serviceName"`
48+
Platforms []string `json:"platforms,omitempty"`
49+
NoBackendRetryAfter string `json:"noBackendRetryAfter"`
50+
ReadTimeout string `json:"readTimeout"`
51+
WriteTimeout string `json:"writeTimeout"`
52+
IdleTimeout string `json:"idleTimeout"`
53+
MaxRequestBody string `json:"maxRequestBody"`
54+
RealIPHeaders []string `json:"realIpHeaders"`
55+
TrustedProxies []string `json:"trustedProxies"`
56+
CBEnabled bool `json:"cbEnabled"`
57+
CBThreshold int `json:"cbThreshold"`
58+
CBTimeout string `json:"cbTimeout"`
59+
StaticPaths []string `json:"staticPaths,omitempty"`
60+
StaticExts []string `json:"staticExts,omitempty"`
6061

6162
// target discovery (DNS SRV)
6263
TargetDiscoveryEnabled bool `json:"targetDiscoveryEnabled"`
@@ -82,6 +83,7 @@ type JSONRPCEndpointInfo struct {
8283
SchemaRefresh string `json:"schemaRefresh,omitempty"`
8384
MethodWhitelist bool `json:"methodWhitelist"`
8485
MaxBatchSize int `json:"maxBatchSize"`
86+
MCPMode bool `json:"mcpMode"`
8587
}
8688

8789
type WAFSection struct {
@@ -91,11 +93,22 @@ type WAFSection struct {
9193
}
9294

9395
type RateLimitSection struct {
94-
Enabled bool `json:"enabled"`
95-
PerIP string `json:"perIp"`
96-
Action string `json:"action"`
97-
MaxCounters int `json:"maxCounters"`
98-
Rules []RuleInfo `json:"rules"`
96+
Enabled bool `json:"enabled"`
97+
PerIP string `json:"perIp"`
98+
Action string `json:"action"`
99+
MaxCounters int `json:"maxCounters"`
100+
Rules []RuleInfo `json:"rules"`
101+
URLRules []URLRuleInfo `json:"urlRules"`
102+
}
103+
104+
// URLRuleInfo describes a per-URL rate limit rule.
105+
type URLRuleInfo struct {
106+
Name string `json:"name"`
107+
Path []string `json:"path,omitempty"`
108+
Method []string `json:"method,omitempty"`
109+
Host []string `json:"host,omitempty"`
110+
Limit string `json:"limit"`
111+
Action string `json:"action,omitempty"`
99112
}
100113

101114
type IPSection struct {

internal/dashboard/dashboard_zenrpc.go

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/dashboard/web/builder/builder-config.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function builderConfig() {
1010
targets: ['http://localhost:3000'],
1111
serviceName: '',
1212
platforms: [],
13+
noBackendRetryAfter: '5s',
1314
timeouts: {read: '30s', write: '30s', idle: '120s'},
1415
limits: {maxRequestBody: '1MB'},
1516
realIP: {
@@ -27,7 +28,7 @@ function builderConfig() {
2728
waf: {enabled: false, mode: 'detection', paranoiaLevel: 1},
2829
rateLimit: {
2930
enabled: false, perIP: '100/min', action: 'block', maxCounters: 100000,
30-
rules: [],
31+
rules: [], urlRules: [],
3132
},
3233
ip: {
3334
geoDatabase: '', asnDatabase: '',
@@ -70,13 +71,17 @@ function builderConfig() {
7071
},
7172

7273
defaultEndpoint() {
73-
return {path: '/rpc/', name: 'main', schemaURL: '', schemaRefresh: '5m', methodWhitelist: false, maxBatchSize: 0};
74+
return {path: '/rpc/', name: 'main', schemaURL: '', schemaRefresh: '5m', methodWhitelist: false, maxBatchSize: 0, mcpMode: false};
7475
},
7576

7677
defaultRateLimitRule() {
7778
return {name: '', endpoint: '', match: [], limit: '10/min', action: ''};
7879
},
7980

81+
defaultRateLimitURLRule() {
82+
return {name: '', path: [], method: [], host: [], limit: '10/min', action: ''};
83+
},
84+
8085
defaultSigningMethod() {
8186
return {name: '', endpoint: '', methods: [], platforms: [], signFields: []};
8287
},
@@ -280,6 +285,10 @@ function builderConfig() {
280285
if (p.rateLimit.perIP) base.rateLimit.perIP = p.rateLimit.perIP;
281286
if (p.rateLimit.action) base.rateLimit.action = p.rateLimit.action;
282287
if (p.rateLimit.rules) base.rateLimit.rules = p.rateLimit.rules.map(r => ({...this.defaultRateLimitRule(), ...r, match: [...(r.match || [])]}));
288+
if (p.rateLimit.urlRules) base.rateLimit.urlRules = p.rateLimit.urlRules.map(r => ({
289+
...this.defaultRateLimitURLRule(), ...r,
290+
path: [...(r.path || [])], method: [...(r.method || [])], host: [...(r.host || [])],
291+
}));
283292
}
284293

285294
// IP

internal/dashboard/web/builder/builder-import.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ function builderImport() {
168168
if (p.Targets) cfg.proxy.targets = [...p.Targets];
169169
if (p.ServiceName) cfg.proxy.serviceName = p.ServiceName;
170170
if (p.Platforms) cfg.proxy.platforms = [...p.Platforms];
171+
if (p.NoBackendRetryAfter) cfg.proxy.noBackendRetryAfter = p.NoBackendRetryAfter;
171172
if (p.Timeouts) {
172173
if (p.Timeouts.Read) cfg.proxy.timeouts.read = p.Timeouts.Read;
173174
if (p.Timeouts.Write) cfg.proxy.timeouts.write = p.Timeouts.Write;
@@ -195,7 +196,7 @@ function builderImport() {
195196
...this.defaultEndpoint(),
196197
path: e.Path || '/rpc/', name: e.Name || 'main',
197198
schemaURL: e.SchemaURL || '', schemaRefresh: e.SchemaRefresh || '5m',
198-
methodWhitelist: e.MethodWhitelist || false, maxBatchSize: e.MaxBatchSize || 0,
199+
methodWhitelist: e.MethodWhitelist || false, maxBatchSize: e.MaxBatchSize || 0, mcpMode: e.MCPMode || false,
199200
}));
200201
}
201202

@@ -222,6 +223,17 @@ function builderImport() {
222223
match: r.Match ? [...r.Match] : [], limit: r.Limit || '10/min', action: r.Action || '',
223224
}));
224225
}
226+
if (t.RateLimit.URLRules) {
227+
cfg.rateLimit.urlRules = t.RateLimit.URLRules.map(r => ({
228+
...this.defaultRateLimitURLRule(),
229+
name: r.Name || '',
230+
path: r.Path ? [...r.Path] : [],
231+
method: r.Method ? [...r.Method] : [],
232+
host: r.Host ? [...r.Host] : [],
233+
limit: r.Limit || '10/min',
234+
action: r.Action || '',
235+
}));
236+
}
225237
}
226238

227239
// IP

internal/dashboard/web/builder/builder-toml.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ function builderToml() {
3636
if (c.proxy.platforms.length > 0) {
3737
lines.push(`Platforms = [${c.proxy.platforms.map(p => this._q(p)).join(', ')}]`);
3838
}
39+
if (c.proxy.noBackendRetryAfter !== D.proxy.noBackendRetryAfter) {
40+
lines.push(`NoBackendRetryAfter = ${this._q(c.proxy.noBackendRetryAfter)}`);
41+
}
3942

4043
// Timeouts
4144
if (c.proxy.timeouts.read !== D.proxy.timeouts.read ||
@@ -106,7 +109,7 @@ function builderToml() {
106109
// --- JSONRPC ---
107110
_genJSONRPC(c, D, lines) {
108111
const hasNonDefault = c.jsonrpc.endpoints.some(ep =>
109-
ep.path !== '/rpc/' || ep.name !== 'main' || ep.schemaURL || ep.methodWhitelist || ep.maxBatchSize > 0
112+
ep.path !== '/rpc/' || ep.name !== 'main' || ep.schemaURL || ep.methodWhitelist || ep.maxBatchSize > 0 || ep.mcpMode
110113
) || c.jsonrpc.endpoints.length !== 1;
111114

112115
if (hasNonDefault) {
@@ -121,6 +124,7 @@ function builderToml() {
121124
}
122125
if (ep.methodWhitelist) lines.push('MethodWhitelist = true');
123126
if (ep.maxBatchSize > 0) lines.push(`MaxBatchSize = ${ep.maxBatchSize}`);
127+
if (ep.mcpMode) lines.push('MCPMode = true');
124128
}
125129
}
126130
},
@@ -168,6 +172,19 @@ function builderToml() {
168172
lines.push(`Limit = ${this._q(r.limit)}`);
169173
if (r.action) lines.push(`Action = ${this._q(r.action)}`);
170174
}
175+
176+
const urlRules = c.rateLimit.urlRules || [];
177+
for (const r of urlRules) {
178+
if (!r.name) continue;
179+
lines.push('');
180+
lines.push('[[RateLimit.URLRules]]');
181+
lines.push(`Name = ${this._q(r.name)}`);
182+
if (r.path && r.path.length > 0) lines.push(`Path = [${r.path.map(s => this._q(s)).join(', ')}]`);
183+
if (r.method && r.method.length > 0) lines.push(`Method = [${r.method.map(s => this._q(s)).join(', ')}]`);
184+
if (r.host && r.host.length > 0) lines.push(`Host = [${r.host.map(s => this._q(s)).join(', ')}]`);
185+
lines.push(`Limit = ${this._q(r.limit)}`);
186+
if (r.action) lines.push(`Action = ${this._q(r.action)}`);
187+
}
171188
},
172189

173190
// --- IP ---

internal/dashboard/web/builder/builder.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ function builder() {
148148
// --- RateLimit Rules ---
149149
addRateLimitRule() { this.cfg.rateLimit.rules.push(this.defaultRateLimitRule()); this.onChange(); },
150150
removeRateLimitRule(i) { this.cfg.rateLimit.rules.splice(i, 1); this.onChange(); },
151+
addRateLimitURLRule() {
152+
if (!this.cfg.rateLimit.urlRules) this.cfg.rateLimit.urlRules = [];
153+
this.cfg.rateLimit.urlRules.push(this.defaultRateLimitURLRule());
154+
this.onChange();
155+
},
156+
removeRateLimitURLRule(i) { this.cfg.rateLimit.urlRules.splice(i, 1); this.onChange(); },
151157

152158
// --- Traffic Filter Rules ---
153159
addTrafficRule() { this.cfg.trafficFilter.rules.push(this.defaultTrafficRule()); this.onChange(); },

0 commit comments

Comments
 (0)