fix(deploy): prevent Caddy compression from buffering SSE
Replace the broad text response matcher with an explicit non-SSE MIME allowlist. Document proxy behavior and enforce the canonical Caddy compression policy in CI.
This commit is contained in:
@@ -16,6 +16,7 @@ jobs:
|
||||
run: |
|
||||
/bin/bash -n deploy/apple-container.sh
|
||||
/bin/bash deploy/tests/apple-container-test.sh
|
||||
/bin/sh deploy/test-caddyfile-cache.sh
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+8
-1
@@ -67,7 +67,14 @@ api.sub2api.com {
|
||||
gzip 6
|
||||
minimum_length 256
|
||||
match {
|
||||
header Content-Type text/*
|
||||
# 不使用 text/*,否则 text/event-stream 会被压缩并缓冲到流结束
|
||||
header Content-Type text/css*
|
||||
header Content-Type text/csv*
|
||||
header Content-Type text/html*
|
||||
header Content-Type text/javascript*
|
||||
header Content-Type text/markdown*
|
||||
header Content-Type text/plain*
|
||||
header Content-Type text/xml*
|
||||
header Content-Type application/json*
|
||||
header Content-Type application/javascript*
|
||||
header Content-Type application/xml*
|
||||
|
||||
@@ -128,6 +128,20 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
If Nginx gzip is enabled in the `http` block, keep `text/event-stream` out of
|
||||
`gzip_types` and do not use `gzip_types *` for Sub2API. The
|
||||
`proxy_buffering off` setting above prevents proxy buffering, but it does not
|
||||
disable the gzip response filter. Use an explicit list for ordinary responses:
|
||||
|
||||
```nginx
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;
|
||||
```
|
||||
|
||||
If a shared global configuration cannot exclude SSE by content type, set
|
||||
`gzip off;` in the locations serving streaming API routes. This leaves gzip
|
||||
available for the web UI and static assets.
|
||||
|
||||
Do not use an incoming `$http_x_forwarded_for` value unless Nginx real-IP
|
||||
processing is restricted to explicit trusted proxy CIDRs.
|
||||
|
||||
@@ -140,6 +154,17 @@ the TCP peer. It is therefore a direct-to-Caddy baseline. Do not use its
|
||||
attributed to a CDN egress address, collapsing rejection aggregation and the
|
||||
invalid-auth limiter onto unrelated users.
|
||||
|
||||
The bundled Caddy configuration leaves `flush_interval` unset so Caddy can
|
||||
automatically flush `text/event-stream` responses while still propagating
|
||||
client cancellation upstream. Do not set it globally: positive values can add
|
||||
streaming latency, while Caddy 2.6.2's special `-1` mode also causes
|
||||
reverse-proxied requests to continue after clients disconnect. The
|
||||
configuration uses an explicit response content-type list for compression. Do
|
||||
not replace that list with `text/*` or the shorthand `encode gzip zstd`: both
|
||||
match `text/event-stream` and can buffer SSE until the response ends. Keep
|
||||
streaming responses uncompressed while retaining compression for the web UI,
|
||||
JSON, and static assets.
|
||||
|
||||
For a CDN deployment, first firewall the origin so only current CDN egress
|
||||
CIDRs can connect. Then configure those exact ranges as Caddy trusted proxies
|
||||
and derive upstream headers from Caddy's parsed `{client_ip}`. For example:
|
||||
|
||||
@@ -4,15 +4,82 @@ set -eu
|
||||
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
caddyfile="$repo_root/deploy/Caddyfile"
|
||||
active_config=$(sed 's/[[:space:]]*#.*$//' "$caddyfile")
|
||||
normalized_config=$(printf '%s\n' "$active_config" | awk '
|
||||
NF > 0 {
|
||||
for (field = 1; field <= NF; field++) {
|
||||
token = $field
|
||||
sub(/^["`]/, "", token)
|
||||
sub(/["`]$/, "", token)
|
||||
printf "%s%s", field == 1 ? "" : " ", token
|
||||
}
|
||||
print ""
|
||||
}
|
||||
')
|
||||
|
||||
if printf '%s\n' "$active_config" | grep -Eiq 'Cache-Control.*immutable'; then
|
||||
# Keep one canonical encode block instead of reimplementing Caddy matcher semantics.
|
||||
expected_encode_block=$(cat <<'EOF'
|
||||
encode {
|
||||
zstd
|
||||
gzip 6
|
||||
minimum_length 256
|
||||
match {
|
||||
header Content-Type text/css*
|
||||
header Content-Type text/csv*
|
||||
header Content-Type text/html*
|
||||
header Content-Type text/javascript*
|
||||
header Content-Type text/markdown*
|
||||
header Content-Type text/plain*
|
||||
header Content-Type text/xml*
|
||||
header Content-Type application/json*
|
||||
header Content-Type application/javascript*
|
||||
header Content-Type application/xml*
|
||||
header Content-Type application/rss+xml*
|
||||
header Content-Type image/svg+xml*
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
if printf '%s\n' "$normalized_config" | grep -Eiq 'cache-control.*immutable'; then
|
||||
echo "Caddyfile must not force immutable caching; the backend owns asset cache policy" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf '%s\n' "$active_config" | grep -Eq '^[[:space:]]*reverse_proxy[[:space:]]+localhost:8080'; then
|
||||
if ! printf '%s\n' "$normalized_config" | grep -Eq '^reverse_proxy localhost:8080([[:space:]]|$)'; then
|
||||
echo "Caddyfile must continue proxying all application routes to localhost:8080" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Caddyfile preserves backend Cache-Control policy and reverse_proxy routing"
|
||||
if printf '%s\n' "$normalized_config" | grep -Eq '^import([[:space:]]|$)'; then
|
||||
echo "Caddyfile must not import configuration outside this canonical policy check" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$normalized_config" | grep -Eiq '^flush_interval([[:space:]]|$)'; then
|
||||
echo "Caddyfile must leave flush_interval unset so SSE auto-flushing and client cancellation remain intact" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
encode_directive_count=$(printf '%s\n' "$normalized_config" | awk '$1 == "encode" { count++ } END { print count + 0 }')
|
||||
if [ "$encode_directive_count" -ne 1 ]; then
|
||||
echo "Caddyfile must contain exactly one explicit encode block" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actual_encode_block=$(printf '%s\n' "$normalized_config" | awk '
|
||||
$1 == "encode" { in_block = 1 }
|
||||
in_block {
|
||||
print
|
||||
for (field = 1; field <= NF; field++) {
|
||||
if ($field == "{") depth++
|
||||
if ($field == "}") depth--
|
||||
}
|
||||
if (depth == 0) exit
|
||||
}
|
||||
')
|
||||
if [ "$actual_encode_block" != "$expected_encode_block" ]; then
|
||||
echo "Caddyfile encode block must keep the canonical non-SSE compression policy" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Caddyfile preserves backend cache policy, SSE streaming, and non-SSE compression"
|
||||
|
||||
Reference in New Issue
Block a user