概念定义
Content-Encoding 是表示层头部,指明应用于响应体的压缩算法,由客户端在 Accept-Encoding 中列出的支持集合协商选定。gzip 普适,brotli 以更高 CPU 代价进一步压缩文本,zstd 提供速度与压缩比的可调折中。编码与传输编码正交,并随表示一起进入缓存。
工作机制
Accept-Encoding 列出带质量值的令牌。服务器按内容类型与大小阈值选择算法;动态压缩按响应逐次执行并消耗源站 CPU,而静态资源可以预先以最高级别压缩后免费分发。zstd 支持带显式刷新点的流式压缩,适合事件流。
使用场景
所有文本都应压缩:标记、样式、脚本、JSON 以及适用的字体。静态资源预压缩到最高级别。流式响应必须校验压缩刷新与事件边界对齐,否则首事件延迟会倒退。
常见误区
对已压缩媒体再压缩纯属浪费 CPU 还可能变大。忽略 Vary 的共享缓存会把 brotli 响应发给只支持 gzip 的客户端,直接弄坏页面。压缩与反射输入叠加曾催生 BREACH 类攻击,同一压缩上下文混合机密与反射内容时需谨慎设计。
代码示例
export function parseAcceptHeader(header) {
if (typeof header !== "string" || header.trim() === "") {
return [{ type: "*", subtype: "*", q: 1, params: {}, order: 0 }];
}
const ranges = [];
for (const [order, part] of header.split(",").entries()) {
const segments = part.split(";").map((segment) => segment.trim());
const mediaRange = segments.shift();
if (!mediaRange) continue;
const slash = mediaRange.indexOf("/");
if (slash <= 0 || slash === mediaRange.length - 1) {
throw new SyntaxError("invalid media range: " + mediaRange);
}
const type = mediaRange.slice(0, slash).toLowerCase();
const subtype = mediaRange.slice(slash + 1).toLowerCase();
const params = {};
let q = 1;
for (const segment of segments) {
if (!segment) continue;
const equals = segment.indexOf("=");
if (equals <= 0) throw new SyntaxError("invalid parameter: " + segment);
const name = segment.slice(0, equals).trim().toLowerCase();
const raw = segment.slice(equals + 1).trim().replace(/^"|"$/g, "");
if (name === "q") {
q = Number(raw);
if (!Number.isFinite(q) || q < 0 || q > 1) {
throw new RangeError("invalid q value: " + raw);
}
} else {
params[name] = raw;
}
}
ranges.push({ type, subtype, q, params, order });
}
if (ranges.length === 0) throw new SyntaxError("Accept header has no media ranges");
return ranges;
}
export function negotiateContentType(header, supported) {
if (!Array.isArray(supported) || supported.length === 0) {
throw new TypeError("supported must be a non-empty array");
}
const ranges = parseAcceptHeader(header);
const candidates = [];
for (const [order, mediaType] of supported.entries()) {
const slash = mediaType.indexOf("/");
if (slash <= 0) throw new TypeError("invalid supported media type: " + mediaType);
const type = mediaType.slice(0, slash).toLowerCase();
const subtype = mediaType.slice(slash + 1).toLowerCase();
let best;
for (const range of ranges) {
const typeMatches = range.type === "*" || range.type === type;
const subtypeMatches = range.subtype === "*" || range.subtype === subtype;
if (!typeMatches || !subtypeMatches) continue;
const specificity = (range.type === "*" ? 0 : 1) + (range.subtype === "*" ? 0 : 1);
if (!best || specificity > best.specificity ||
(specificity === best.specificity && range.order < best.order)) {
best = { q: range.q, specificity, order: range.order };
}
}
if (best && best.q > 0) {
candidates.push({ mediaType, q: best.q, specificity: best.specificity, order });
}
}
candidates.sort((a, b) => b.q - a.q || b.specificity - a.specificity || a.order - b.order);
return candidates.length > 0 ? candidates[0].mediaType : null;
}
package main
import (
"compress/gzip"
"fmt"
"net/http"
"strings"
)
type gzipWriter struct {
http.ResponseWriter
writer *gzip.Writer
}
func (g *gzipWriter) Write(data []byte) (int, error) {
return g.writer.Write(data)
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Vary", "Accept-Encoding")
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
compressor := gzip.NewWriter(w)
defer compressor.Close()
next.ServeHTTP(&gzipWriter{ResponseWriter: w, writer: compressor}, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintln(w, strings.Repeat("hello gzip ", 64))
})
_ = http.ListenAndServe(":8080", GzipMiddleware(mux))
}