feat(backends+domains): HTTP-Protokoll-Flags — force_http1 + disable_h3

Backend: force_http1 (bool, default false) — zwingt HAProxy auf der
Backend-Verbindung zu HTTP/1.1 statt h2,http/1.1 zu verhandeln.
Nötig für Legacy-Apps die kein h2 sprechen.

Domain: disable_h3 (bool, default false) — unterdrückt Alt-Svc-
Response-Header für diese Domain. Browser erhalten keinen h3/QUIC-
Hinweis und bleiben auf h2/http1.1.

Migration 0027, Model+Service+HAProxy-Template+UI+i18n.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-22 07:18:48 +02:00
parent 6445e162a6
commit 8293783fe6
11 changed files with 78 additions and 24 deletions

View File

@@ -1 +1 @@
1.1.54 1.1.55

View File

@@ -0,0 +1,22 @@
-- +goose Up
-- +goose StatementBegin
-- backends.force_http1: zwingt HAProxy, die Backend-Verbindung mit
-- HTTP/1.1 zu führen (alpn http/1.1) statt H2+H1.1 zu verhandeln.
-- Nötig für Backends die kein h2 sprechen (Legacy-Apps, manche nginx-Configs).
ALTER TABLE backends
ADD COLUMN IF NOT EXISTS force_http1 BOOLEAN NOT NULL DEFAULT FALSE;
-- domains.disable_h3: unterdrückt den Alt-Svc-Response-Header für diese
-- Domain. Browser erhalten keinen Hinweis auf h3/QUIC und bleiben auf
-- h2/http1.1 — sinnvoll wenn Clients Probleme mit QUIC-Verbindungen melden.
ALTER TABLE domains
ADD COLUMN IF NOT EXISTS disable_h3 BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE backends DROP COLUMN IF EXISTS force_http1;
ALTER TABLE domains DROP COLUMN IF EXISTS disable_h3;
-- +goose StatementEnd

View File

@@ -108,6 +108,13 @@ frontend public_https
http-response del-header Strict-Transport-Security if { hdr(host) -i {{$d.Name}} } http-response del-header Strict-Transport-Security if { hdr(host) -i {{$d.Name}} }
http-response set-header Strict-Transport-Security "{{$d.HSTSHeader}}" if { hdr(host) -i {{$d.Name}} } http-response set-header Strict-Transport-Security "{{$d.HSTSHeader}}" if { hdr(host) -i {{$d.Name}} }
{{- end}} {{- end}}
{{- if $d.DisableH3}}
# HTTP/3 (QUIC) Werbung für diese Domain unterdrücken: Alt-Svc-Header
# entfernen damit Browser nicht auf h3 upgraden. Der globale set-header
# oben hat ihn bereits gesetzt; dieses del-header kommt DANACH und
# überschreibt ihn für Requests an diese Domain.
http-response del-header Alt-Svc if { hdr(host) -i {{$d.Name}} }
{{- end}}
{{- range $h := $d.ResponseHeaders}} {{- range $h := $d.ResponseHeaders}}
http-response del-header {{$h.Name}} if { hdr(host) -i {{$d.Name}} } http-response del-header {{$h.Name}} if { hdr(host) -i {{$d.Name}} }
http-response set-header {{$h.Name}} "{{$h.Value}}" if { hdr(host) -i {{$d.Name}} } http-response set-header {{$h.Name}} "{{$h.Value}}" if { hdr(host) -i {{$d.Name}} }
@@ -176,6 +183,6 @@ backend eg_backend_{{$b.ID}}
http-check send meth GET uri {{$b.HealthCheckPath}} http-check send meth GET uri {{$b.HealthCheckPath}}
{{- end}} {{- end}}
{{- range $s := $b.Servers}} {{- range $s := $b.Servers}}
server {{$s.Name | safeID}} {{$s.Address}}:{{$s.Port}}{{if eq $b.Scheme "https"}} ssl verify none alpn h2,http/1.1{{end}}{{if $b.HealthCheckPath}} check inter 5s{{if eq $b.Scheme "https"}} check-alpn http/1.1{{end}}{{end}} weight {{$s.Weight}}{{if $s.Backup}} backup{{end}} server {{$s.Name | safeID}} {{$s.Address}}:{{$s.Port}}{{if eq $b.Scheme "https"}} ssl verify none alpn {{if $b.ForceHTTP1}}http/1.1{{else}}h2,http/1.1{{end}}{{end}}{{if $b.HealthCheckPath}} check inter 5s{{if eq $b.Scheme "https"}} check-alpn http/1.1{{end}}{{end}} weight {{$s.Weight}}{{if $s.Backup}} backup{{end}}
{{- end}} {{- end}}
{{- end}} {{- end}}

View File

@@ -11,6 +11,7 @@ type Backend struct {
HealthCheckPath *string `gorm:"column:health_check_path" json:"health_check_path,omitempty"` HealthCheckPath *string `gorm:"column:health_check_path" json:"health_check_path,omitempty"`
LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"` LBAlgorithm string `gorm:"column:lb_algorithm" json:"lb_algorithm"`
WebSocket bool `gorm:"column:websocket" json:"websocket"` WebSocket bool `gorm:"column:websocket" json:"websocket"`
ForceHTTP1 bool `gorm:"column:force_http1" json:"force_http1"`
Active bool `gorm:"column:active" json:"active"` Active bool `gorm:"column:active" json:"active"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`

View File

@@ -17,6 +17,7 @@ type Domain struct {
WWWRedirect string `gorm:"column:www_redirect" json:"www_redirect"` // ""|"to-naked"|"to-www" WWWRedirect string `gorm:"column:www_redirect" json:"www_redirect"` // ""|"to-naked"|"to-www"
RateLimitRPS int `gorm:"column:rate_limit_rps" json:"rate_limit_rps"` RateLimitRPS int `gorm:"column:rate_limit_rps" json:"rate_limit_rps"`
MaxBodyKB int `gorm:"column:max_body_kb" json:"max_body_kb"` MaxBodyKB int `gorm:"column:max_body_kb" json:"max_body_kb"`
DisableH3 bool `gorm:"column:disable_h3" json:"disable_h3"`
Notes *string `gorm:"column:notes" json:"notes,omitempty"` Notes *string `gorm:"column:notes" json:"notes,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`

View File

@@ -23,7 +23,7 @@ type Repo struct {
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} } func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
const baseSelect = ` const baseSelect = `
SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, active, SELECT id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
created_at, updated_at created_at, updated_at
FROM backends FROM backends
` `
@@ -62,11 +62,11 @@ func (r *Repo) Create(ctx context.Context, b models.Backend) (*models.Backend, e
b.LBAlgorithm = "roundrobin" b.LBAlgorithm = "roundrobin"
} }
row := r.Pool.QueryRow(ctx, ` row := r.Pool.QueryRow(ctx, `
INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, active) INSERT INTO backends (name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active, RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
created_at, updated_at`, created_at, updated_at`,
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.Active) b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1, b.Active)
return scanBackend(row) return scanBackend(row)
} }
@@ -81,12 +81,13 @@ UPDATE backends SET
health_check_path = $3, health_check_path = $3,
lb_algorithm = $4, lb_algorithm = $4,
websocket = $5, websocket = $5,
active = $6, force_http1 = $6,
active = $7,
updated_at = NOW() updated_at = NOW()
WHERE id = $7 WHERE id = $8
RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, active, RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_http1, active,
created_at, updated_at`, created_at, updated_at`,
b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.Active, id) b.Name, b.Scheme, b.HealthCheckPath, b.LBAlgorithm, b.WebSocket, b.ForceHTTP1, b.Active, id)
out, err := scanBackend(row) out, err := scanBackend(row)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
@@ -112,7 +113,7 @@ func scanBackend(row interface{ Scan(...any) error }) (*models.Backend, error) {
var b models.Backend var b models.Backend
if err := row.Scan( if err := row.Scan(
&b.ID, &b.Name, &b.Scheme, &b.ID, &b.Name, &b.Scheme,
&b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.Active, &b.HealthCheckPath, &b.LBAlgorithm, &b.WebSocket, &b.ForceHTTP1, &b.Active,
&b.CreatedAt, &b.UpdatedAt, &b.CreatedAt, &b.UpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err

View File

@@ -23,7 +23,7 @@ const baseSelect = `
SELECT id, name, active, primary_backend_id, http_to_https, SELECT id, name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload, hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect, maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, rate_limit_rps, max_body_kb, disable_h3,
notes, created_at, updated_at notes, created_at, updated_at
FROM domains FROM domains
` `
@@ -65,17 +65,17 @@ func (r *Repo) Create(ctx context.Context, d models.Domain) (*models.Domain, err
INSERT INTO domains (name, active, primary_backend_id, http_to_https, INSERT INTO domains (name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload, hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect, maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, notes) rate_limit_rps, max_body_kb, disable_h3, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING id, name, active, primary_backend_id, http_to_https, RETURNING id, name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload, hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect, maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, rate_limit_rps, max_body_kb, disable_h3,
notes, created_at, updated_at`, notes, created_at, updated_at`,
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS, d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload, d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect, d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
d.RateLimitRPS, d.MaxBodyKB, d.Notes) d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes)
return scanDomain(row) return scanDomain(row)
} }
@@ -98,18 +98,19 @@ UPDATE domains SET
www_redirect = $11, www_redirect = $11,
rate_limit_rps = $12, rate_limit_rps = $12,
max_body_kb = $13, max_body_kb = $13,
notes = $14, disable_h3 = $14,
notes = $15,
updated_at = NOW() updated_at = NOW()
WHERE id = $15 WHERE id = $16
RETURNING id, name, active, primary_backend_id, http_to_https, RETURNING id, name, active, primary_backend_id, http_to_https,
hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload, hsts_enabled, hsts_max_age, hsts_subdomains, hsts_preload,
maintenance_mode, maintenance_message, www_redirect, maintenance_mode, maintenance_message, www_redirect,
rate_limit_rps, max_body_kb, rate_limit_rps, max_body_kb, disable_h3,
notes, created_at, updated_at`, notes, created_at, updated_at`,
d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS, d.Name, d.Active, d.PrimaryBackendID, d.HTTPToHTTPS,
d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload, d.HSTSEnabled, d.HSTSMaxAge, d.HSTSSubdomains, d.HSTSPreload,
d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect, d.MaintenanceMode, d.MaintenanceMessage, d.WWWRedirect,
d.RateLimitRPS, d.MaxBodyKB, d.Notes, id) d.RateLimitRPS, d.MaxBodyKB, d.DisableH3, d.Notes, id)
out, err := scanDomain(row) out, err := scanDomain(row)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
@@ -137,7 +138,7 @@ func scanDomain(row interface{ Scan(...any) error }) (*models.Domain, error) {
&d.ID, &d.Name, &d.Active, &d.PrimaryBackendID, &d.HTTPToHTTPS, &d.ID, &d.Name, &d.Active, &d.PrimaryBackendID, &d.HTTPToHTTPS,
&d.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload, &d.HSTSEnabled, &d.HSTSMaxAge, &d.HSTSSubdomains, &d.HSTSPreload,
&d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect, &d.MaintenanceMode, &d.MaintenanceMessage, &d.WWWRedirect,
&d.RateLimitRPS, &d.MaxBodyKB, &d.RateLimitRPS, &d.MaxBodyKB, &d.DisableH3,
&d.Notes, &d.CreatedAt, &d.UpdatedAt, &d.Notes, &d.CreatedAt, &d.UpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err

View File

@@ -290,6 +290,8 @@
"rateLimitHint": "Max. Requests pro Sekunde je Client-IP. HAProxy zählt über ein 10-Sekunden-Fenster pro Stick-Table (max. 100k IPs). 0 = aus.", "rateLimitHint": "Max. Requests pro Sekunde je Client-IP. HAProxy zählt über ein 10-Sekunden-Fenster pro Stick-Table (max. 100k IPs). 0 = aus.",
"maxBody": "Max. Request-Body", "maxBody": "Max. Request-Body",
"maxBodyHint": "Cap auf Content-Length-Header. Größere Requests bekommen 413. Nicht erkannt: Chunked-Bodies (HAProxy bufferd nicht standardmäßig). 0 = aus.", "maxBodyHint": "Cap auf Content-Length-Header. Größere Requests bekommen 413. Nicht erkannt: Chunked-Bodies (HAProxy bufferd nicht standardmäßig). 0 = aus.",
"disableH3": "HTTP/3 (QUIC) deaktivieren",
"disableH3Hint": "Unterdrückt den Alt-Svc-Response-Header für diese Domain. Browser erhalten keinen Hinweis auf h3/QUIC und bleiben auf h2/http1.1. Sinnvoll bei Clients die Probleme mit QUIC-Verbindungen melden.",
"headersBtn": "Headers", "headersBtn": "Headers",
"headersTitle": "Response-Headers — {{name}}", "headersTitle": "Response-Headers — {{name}}",
"headersHint": "Diese Header werden von HAProxy auf jede Response für diese Domain gesetzt (http-response set-header). Reihenfolge via Position.", "headersHint": "Diese Header werden von HAProxy auf jede Response für diese Domain gesetzt (http-response set-header). Reihenfolge via Position.",
@@ -331,6 +333,8 @@
"lbAlgoHint": "roundrobin = gleichmäßig, leastconn = an den Server mit wenigsten Verbindungen, source = sticky per Client-IP (für stateful Apps ohne shared session).", "lbAlgoHint": "roundrobin = gleichmäßig, leastconn = an den Server mit wenigsten Verbindungen, source = sticky per Client-IP (für stateful Apps ohne shared session).",
"websocket": "WebSocket-Support", "websocket": "WebSocket-Support",
"websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.", "websocketHint": "An: erlaubt langlebige WebSocket-/Long-Poll-Verbindungen (z. B. Proxmox-Console, SSH-WS, AsyncAPI) — Tunnel-Idle 1h statt 60s. Aus: strikte HTTP-Timeouts.",
"forceHttp1": "HTTP/1.1 erzwingen",
"forceHttp1Hint": "Deaktiviert HTTP/2 (h2) auf der Backend-Verbindung — HAProxy handelt nur noch HTTP/1.1 aus. Nötig für Backends die kein h2 unterstützen (z. B. ältere nginx-Konfigurationen ohne h2-Modul, Legacy-Apps).",
"servers": "Server", "servers": "Server",
"noServers": "kein Server", "noServers": "kein Server",
"nServers": "{{n}} Server", "nServers": "{{n}} Server",

View File

@@ -290,6 +290,8 @@
"rateLimitHint": "Max requests per second per client IP. HAProxy counts over a 10-second window per stick table (max. 100k IPs). 0 = off.", "rateLimitHint": "Max requests per second per client IP. HAProxy counts over a 10-second window per stick table (max. 100k IPs). 0 = off.",
"maxBody": "Max request body", "maxBody": "Max request body",
"maxBodyHint": "Cap on the Content-Length header. Larger requests get 413. Chunked bodies are not detected (HAProxy doesn't buffer by default). 0 = off.", "maxBodyHint": "Cap on the Content-Length header. Larger requests get 413. Chunked bodies are not detected (HAProxy doesn't buffer by default). 0 = off.",
"disableH3": "Disable HTTP/3 (QUIC)",
"disableH3Hint": "Suppresses the Alt-Svc response header for this domain. Browsers won't receive an h3/QUIC upgrade hint and stay on h2/http1.1. Useful when clients report issues with QUIC connections.",
"headersBtn": "Headers", "headersBtn": "Headers",
"headersTitle": "Response headers — {{name}}", "headersTitle": "Response headers — {{name}}",
"headersHint": "These headers are set by HAProxy on every response for this domain (http-response set-header). Ordering via position.", "headersHint": "These headers are set by HAProxy on every response for this domain (http-response set-header). Ordering via position.",
@@ -331,6 +333,8 @@
"lbAlgoHint": "roundrobin = evenly, leastconn = pick the server with fewest active connections, source = sticky per client-IP hash (for stateful apps without shared session).", "lbAlgoHint": "roundrobin = evenly, leastconn = pick the server with fewest active connections, source = sticky per client-IP hash (for stateful apps without shared session).",
"websocket": "WebSocket support", "websocket": "WebSocket support",
"websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.", "websocketHint": "On: allow long-lived WebSocket / long-poll connections (Proxmox console, SSH-over-WS, AsyncAPI) — tunnel idle 1h instead of 60s. Off: strict HTTP timeouts.",
"forceHttp1": "Force HTTP/1.1",
"forceHttp1Hint": "Disables HTTP/2 (h2) on the backend connection — HAProxy negotiates HTTP/1.1 only. Required for backends that don't support h2 (e.g. older nginx configs without the h2 module, legacy apps).",
"servers": "Servers", "servers": "Servers",
"noServers": "no server", "noServers": "no server",
"nServers": "{{n}} servers", "nServers": "{{n}} servers",

View File

@@ -20,13 +20,13 @@ interface Backend {
id: number; name: string; scheme: string id: number; name: string; scheme: string
health_check_path?: string | null health_check_path?: string | null
lb_algorithm: 'roundrobin' | 'leastconn' | 'source' lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
websocket: boolean; active: boolean websocket: boolean; force_http1: boolean; active: boolean
} }
interface BackendFormValues { interface BackendFormValues {
name: string; scheme: 'http' | 'https' name: string; scheme: 'http' | 'https'
health_check_path?: string health_check_path?: string
lb_algorithm: 'roundrobin' | 'leastconn' | 'source' lb_algorithm: 'roundrobin' | 'leastconn' | 'source'
websocket: boolean; active: boolean websocket: boolean; force_http1: boolean; active: boolean
domain_ids?: number[] domain_ids?: number[]
} }
interface BackendServer { interface BackendServer {
@@ -157,6 +157,7 @@ export default function BackendDetailPage() {
health_check_path: backend.health_check_path ?? undefined, health_check_path: backend.health_check_path ?? undefined,
lb_algorithm: backend.lb_algorithm, lb_algorithm: backend.lb_algorithm,
websocket: backend.websocket, websocket: backend.websocket,
force_http1: backend.force_http1,
active: backend.active, active: backend.active,
domain_ids: attached.map(d => d.id), domain_ids: attached.map(d => d.id),
}} }}
@@ -184,6 +185,10 @@ export default function BackendDetailPage() {
extra={t('backends.websocketHint')}> extra={t('backends.websocketHint')}>
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item label={t('backends.forceHttp1')} name="force_http1" valuePropName="checked"
extra={t('backends.forceHttp1Hint')}>
<Switch />
</Form.Item>
<Form.Item label={t('backends.active')} name="active" valuePropName="checked"> <Form.Item label={t('backends.active')} name="active" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>

View File

@@ -27,6 +27,7 @@ interface Domain {
maintenance_mode: boolean; maintenance_message?: string | null maintenance_mode: boolean; maintenance_message?: string | null
www_redirect: '' | 'to-naked' | 'to-www' www_redirect: '' | 'to-naked' | 'to-www'
rate_limit_rps: number; max_body_kb: number rate_limit_rps: number; max_body_kb: number
disable_h3: boolean
notes?: string | null notes?: string | null
} }
@@ -39,6 +40,7 @@ interface DomainFormValues {
maintenance_mode: boolean; maintenance_message?: string maintenance_mode: boolean; maintenance_message?: string
www_redirect: '' | 'to-naked' | 'to-www' www_redirect: '' | 'to-naked' | 'to-www'
rate_limit_rps: number; max_body_kb: number rate_limit_rps: number; max_body_kb: number
disable_h3: boolean
notes?: string notes?: string
} }
@@ -164,6 +166,7 @@ export default function DomainDetailPage() {
www_redirect: domain.www_redirect ?? '', www_redirect: domain.www_redirect ?? '',
rate_limit_rps: domain.rate_limit_rps ?? 0, rate_limit_rps: domain.rate_limit_rps ?? 0,
max_body_kb: domain.max_body_kb ?? 0, max_body_kb: domain.max_body_kb ?? 0,
disable_h3: domain.disable_h3 ?? false,
notes: domain.notes ?? '', notes: domain.notes ?? '',
}} }}
onFinish={(v) => update.mutate(v)} onFinish={(v) => update.mutate(v)}
@@ -244,6 +247,11 @@ export default function DomainDetailPage() {
<InputNumber min={0} step={64} style={{ width: '100%' }} addonAfter="KiB" /> <InputNumber min={0} step={64} style={{ width: '100%' }} addonAfter="KiB" />
</Form.Item> </Form.Item>
<Form.Item label={t('domains.disableH3')} name="disable_h3" valuePropName="checked"
extra={t('domains.disableH3Hint')}>
<Switch />
</Form.Item>
<Form.Item label={t('domains.notes')} name="notes"> <Form.Item label={t('domains.notes')} name="notes">
<Input.TextArea rows={2} /> <Input.TextArea rows={2} />
</Form.Item> </Form.Item>