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

@@ -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.",
"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.",
"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",
"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.",
@@ -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).",
"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.",
"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",
"noServers": "kein 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.",
"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.",
"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",
"headersTitle": "Response headers — {{name}}",
"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).",
"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.",
"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",
"noServers": "no server",
"nServers": "{{n}} servers",

View File

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

View File

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