响应处理 #

一、响应处理概述 #

1.1 响应处理流程 #

text
┌─────────────────────────────────────────────────────────┐
│                    响应处理流程                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│   Backend Response                                      │
│        │                                                │
│        ▼                                                │
│   ┌──────────────────┐                                  │
│   │vcl_backend_response│ ◄─── 后端响应处理              │
│   └────────┬─────────┘                                  │
│            │                                            │
│            └──► 存储缓存                                 │
│                                                         │
│   Cached Object                                         │
│        │                                                │
│        ▼                                                │
│   ┌─────────────┐                                       │
│   │ vcl_deliver │ ◄─── 返回响应                         │
│   └──────┬──────┘                                       │
│          │                                              │
│          ▼                                              │
│   Client Response                                       │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.2 响应处理子程序 #

子程序 触发时机 主要用途
vcl_backend_response 收到后端响应 设置缓存策略
vcl_deliver 返回客户端 修改响应头
vcl_synth 合成响应 自定义错误页面
vcl_backend_error 后端错误 错误处理

二、响应头处理 #

2.1 添加响应头 #

vcl
sub vcl_deliver {
    # 添加缓存状态
    set resp.http.X-Cache = "HIT";
    
    # 添加服务器信息
    set resp.http.X-Served-By = "Varnish";
    
    # 添加请求ID
    set resp.http.X-Request-ID = req.xid;
    
    # 添加处理时间
    set resp.http.X-Response-Time = time.elapsed_ms() + "ms";
}

2.2 修改响应头 #

vcl
sub vcl_deliver {
    # 修改Server头
    set resp.http.Server = "MyServer/1.0";
    
    # 修改Content-Type
    if (resp.http.Content-Type ~ "text/html") {
        set resp.http.Content-Type = "text/html; charset=utf-8";
    }
}

2.3 删除响应头 #

vcl
sub vcl_deliver {
    # 删除敏感头
    unset resp.http.X-Powered-By;
    unset resp.http.Server;
    unset resp.http.X-AspNet-Version;
    unset resp.http.X-AspNetMvc-Version;
    unset resp.http.Via;
    
    # 删除调试头
    unset resp.http.X-Debug-Token;
    unset resp.http.X-Debug-Token-Link;
}

2.4 条件修改 #

vcl
sub vcl_deliver {
    # 根据缓存状态设置头
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }
    
    # 根据内容类型设置
    if (resp.http.Content-Type ~ "text/html") {
        set resp.http.X-Content-Type = "html";
    }
}

三、缓存状态头 #

3.1 基本缓存状态 #

vcl
sub vcl_deliver {
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
}

3.2 详细缓存信息 #

vcl
sub vcl_deliver {
    # 缓存状态
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
        set resp.http.X-Cache-TTL = obj.ttl;
        set resp.http.X-Cache-Grace = obj.grace;
    } else {
        set resp.http.X-Cache = "MISS";
    }
    
    # 后端信息
    set resp.http.X-Backend = beresp.backend.name;
}

3.3 调试信息 #

vcl
sub vcl_deliver {
    # 仅在调试模式下添加
    if (req.http.X-Debug) {
        set resp.http.X-Debug-URL = req.url;
        set resp.http.X-Debug-Method = req.method;
        set resp.http.X-Debug-Host = req.http.Host;
        set resp.http.X-Debug-Client-IP = client.ip;
        set resp.http.X-Debug-Request-ID = req.xid;
    }
}

四、安全头配置 #

4.1 基本安全头 #

vcl
sub vcl_deliver {
    # 防止MIME类型嗅探
    set resp.http.X-Content-Type-Options = "nosniff";
    
    # XSS保护
    set resp.http.X-XSS-Protection = "1; mode=block";
    
    # 点击劫持保护
    set resp.http.X-Frame-Options = "SAMEORIGIN";
    
    # Referrer策略
    set resp.http.Referrer-Policy = "strict-origin-when-cross-origin";
}

4.2 Content-Security-Policy #

vcl
sub vcl_deliver {
    # CSP策略
    set resp.http.Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
}

4.3 HSTS #

vcl
sub vcl_deliver {
    # HTTP严格传输安全
    set resp.http.Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload";
}

4.4 Permissions-Policy #

vcl
sub vcl_deliver {
    # 权限策略
    set resp.http.Permissions-Policy = "geolocation=(), microphone=(), camera=()";
}

4.5 条件安全头 #

vcl
sub vcl_deliver {
    # HTML页面添加完整安全头
    if (resp.http.Content-Type ~ "text/html") {
        set resp.http.X-Content-Type-Options = "nosniff";
        set resp.http.X-XSS-Protection = "1; mode=block";
        set resp.http.X-Frame-Options = "SAMEORIGIN";
        set resp.http.Content-Security-Policy = "default-src 'self';";
    }
    
    # 静态资源简化安全头
    if (resp.http.Content-Type ~ "image/|text/css|application/javascript") {
        set resp.http.X-Content-Type-Options = "nosniff";
    }
}

五、错误页面处理 #

5.1 自定义错误页面 #

vcl
sub vcl_synth {
    set resp.http.Content-Type = "text/html; charset=utf-8";
    
    if (resp.status == 403) {
        synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>403 Forbidden</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        h1 { color: #e74c3c; }
    </style>
</head>
<body>
    <h1>403 Forbidden</h1>
    <p>Access to this resource is forbidden.</p>
</body>
</html>"});
    } elseif (resp.status == 404) {
        synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>404 Not Found</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        h1 { color: #e74c3c; }
    </style>
</head>
<body>
    <h1>404 Not Found</h1>
    <p>The requested page was not found.</p>
</body>
</html>"});
    } elseif (resp.status == 429) {
        synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>429 Too Many Requests</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        h1 { color: #f39c12; }
    </style>
</head>
<body>
    <h1>429 Too Many Requests</h1>
    <p>Please slow down your requests.</p>
</body>
</html>"});
    } else {
        synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>"} + resp.status + " " + resp.reason + {"</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        h1 { color: #e74c3c; }
    </style>
</head>
<body>
    <h1>"} + resp.status + " " + resp.reason + {"</h1>
    <p>"} + resp.reason + {"</p>
</body>
</html>"});
    }
    
    return (deliver);
}

5.2 后端错误处理 #

vcl
sub vcl_backend_error {
    set beresp.http.Content-Type = "text/html; charset=utf-8";
    synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>503 Service Unavailable</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        h1 { color: #e74c3c; }
    </style>
</head>
<body>
    <h1>503 Service Unavailable</h1>
    <p>The server is temporarily unavailable.</p>
    <p>Please try again later.</p>
</body>
</html>"});
    return (deliver);
}

5.3 JSON错误响应 #

vcl
sub vcl_synth {
    if (req.http.Accept ~ "application/json") {
        set resp.http.Content-Type = "application/json";
        synthetic({"{"error":{"code":} + resp.status + {","message":"} + resp.reason + {"}}"});
    } else {
        set resp.http.Content-Type = "text/html; charset=utf-8";
        synthetic({"<!DOCTYPE html>
<html>
<head><title>"} + resp.status + {"</title></head>
<body><h1>"} + resp.status + " " + resp.reason + {"</h1></body>
</html>"});
    }
    return (deliver);
}

六、压缩处理 #

6.1 Gzip压缩 #

Varnish本身不进行压缩,但可以处理已压缩的响应:

vcl
sub vcl_backend_response {
    # 保留后端压缩
    # Varnish会自动处理Accept-Encoding
    
    # 确保压缩响应正确缓存
    if (beresp.http.Content-Encoding == "gzip") {
        # 已压缩响应
    }
}

6.2 压缩协商 #

vcl
sub vcl_hash {
    # 根据Accept-Encoding区分缓存
    if (req.http.Accept-Encoding ~ "gzip") {
        hash_data("gzip");
    } elseif (req.http.Accept-Encoding ~ "deflate") {
        hash_data("deflate");
    } else {
        hash_data("none");
    }
}

6.3 移除压缩头(可选) #

vcl
sub vcl_recv {
    # 移除Accept-Encoding以获取未压缩内容
    if (req.url ~ "\.(css|js)$") {
        unset req.http.Accept-Encoding;
    }
}

七、缓存控制头 #

7.1 设置Cache-Control #

vcl
sub vcl_deliver {
    # 静态资源
    if (req.url ~ "\.(css|js|png|gif|jpg|jpeg|ico|svg|woff|woff2)$") {
        set resp.http.Cache-Control = "public, max-age=31536000, immutable";
    }
    # HTML
    elseif (resp.http.Content-Type ~ "text/html") {
        set resp.http.Cache-Control = "public, max-age=300, must-revalidate";
    }
    # API
    elseif (req.url ~ "^/api/") {
        set resp.http.Cache-Control = "public, max-age=10";
    }
}

7.2 设置Expires #

vcl
sub vcl_deliver {
    # 设置过期时间
    if (req.url ~ "\.(css|js|png|gif|jpg|jpeg|ico|svg)$") {
        set resp.http.Expires = now + 365d;
    }
}

7.3 ETag处理 #

vcl
sub vcl_deliver {
    # 保留ETag用于条件请求
    # Varnish自动处理If-None-Match
    
    # 或移除ETag
    unset resp.http.ETag;
}

八、响应体修改 #

8.1 使用synthetic #

vcl
sub vcl_synth {
    set resp.http.Content-Type = "text/html";
    synthetic({"<html><body>Custom Response</body></html>"});
    return (deliver);
}

8.2 响应体注入 #

需要使用VMOD:

vcl
import bodyaccess;

sub vcl_deliver {
    # 修改响应体(需要VMOD支持)
    # 基本VCL不支持直接修改响应体
}

九、条件响应 #

9.1 基于客户端 #

vcl
sub vcl_deliver {
    # 内部用户显示调试信息
    if (client.ip ~ internal) {
        set resp.http.X-Debug-Info = "enabled";
    }
}

9.2 基于请求头 #

vcl
sub vcl_deliver {
    # 移动端特殊处理
    if (req.http.User-Agent ~ "Mobile") {
        set resp.http.X-Mobile-Optimized = "true";
    }
    
    # API客户端
    if (req.http.X-API-Client) {
        set resp.http.X-API-Version = "1.0";
    }
}

9.3 基于响应状态 #

vcl
sub vcl_deliver {
    # 错误响应特殊处理
    if (resp.status >= 400) {
        set resp.http.Cache-Control = "no-cache, no-store, must-revalidate";
        unset resp.http.ETag;
    }
}

十、完整配置示例 #

vcl
vcl 4.1;

import std;

# 后端定义
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

# 后端响应处理
sub vcl_backend_response {
    # 设置TTL
    if (bereq.url ~ "\.(css|js|png|gif|jpg|jpeg|ico|svg|woff|woff2)$") {
        set beresp.ttl = 7d;
        unset beresp.http.Set-Cookie;
    } elseif (beresp.http.Content-Type ~ "text/html") {
        set beresp.ttl = 5m;
        set beresp.grace = 1h;
    } else {
        set beresp.ttl = 5m;
    }
    
    # 错误响应不缓存
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
    }
}

# 响应处理
sub vcl_deliver {
    # 缓存状态
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }
    
    # 安全头
    set resp.http.X-Content-Type-Options = "nosniff";
    set resp.http.X-XSS-Protection = "1; mode=block";
    set resp.http.X-Frame-Options = "SAMEORIGIN";
    set resp.http.Referrer-Policy = "strict-origin-when-cross-origin";
    
    # HTML页面额外安全头
    if (resp.http.Content-Type ~ "text/html") {
        set resp.http.Content-Security-Policy = "default-src 'self';";
        set resp.http.Strict-Transport-Security = "max-age=31536000";
    }
    
    # 移除敏感头
    unset resp.http.X-Powered-By;
    unset resp.http.Server;
    unset resp.http.Via;
    
    # 添加请求ID
    set resp.http.X-Request-ID = req.xid;
    
    # Cache-Control
    if (req.url ~ "\.(css|js|png|gif|jpg|jpeg|ico|svg|woff|woff2)$") {
        set resp.http.Cache-Control = "public, max-age=31536000, immutable";
    }
}

# 合成响应
sub vcl_synth {
    set resp.http.Content-Type = "text/html; charset=utf-8";
    
    # 安全头
    set resp.http.X-Content-Type-Options = "nosniff";
    set resp.http.X-Frame-Options = "DENY";
    
    synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>"} + resp.status + " " + resp.reason + {"</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; text-align: center; padding: 50px; background: #f5f5f5; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #e74c3c; margin-bottom: 10px; }
        p { color: #666; }
        .code { font-size: 48px; font-weight: bold; color: #e74c3c; }
    </style>
</head>
<body>
    <div class="container">
        <div class="code">"} + resp.status + {"</div>
        <h1>"} + resp.reason + {"</h1>
        <p>"} + resp.reason + {"</p>
    </div>
</body>
</html>"});
    return (deliver);
}

# 后端错误
sub vcl_backend_error {
    set beresp.http.Content-Type = "text/html; charset=utf-8";
    synthetic({"<!DOCTYPE html>
<html>
<head>
    <title>503 Service Unavailable</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; text-align: center; padding: 50px; background: #f5f5f5; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #e74c3c; }
    </style>
</head>
<body>
    <div class="container">
        <h1>503 Service Unavailable</h1>
        <p>The server is temporarily unavailable.</p>
        <p>Please try again later.</p>
    </div>
</body>
</html>"});
    return (deliver);
}

十一、总结 #

本章我们学习了:

  1. 响应处理概述:流程、子程序
  2. 响应头处理:添加、修改、删除
  3. 缓存状态头:HIT/MISS、调试信息
  4. 安全头配置:CSP、HSTS、X-Frame-Options
  5. 错误页面:自定义错误页面、JSON错误
  6. 压缩处理:Gzip协商
  7. 缓存控制头:Cache-Control、Expires
  8. 条件响应:基于客户端、请求头、状态

掌握响应处理后,让我们进入下一章,学习ESI边缘包含!

最后更新:2026-03-28