浏览器在执行跨域非简单请求前,会先发出一个 OPTIONS 预检请求。这个预检请求会通过 Access-Control-Request-Headers 头,告诉服务器后续真实请求将携带哪些自定义请求头。Nginx 作为反向代理处在链路最前端,它如何记录这个请求头,以及回源到上游服务时是否完整透传,直接决定了后端跨域校验能不能通过。有些开发者在后端日志里看到 Access-Control-Request-Headers 为空,却不知道问题出在 Nginx 还是更上游。本文从日志变量映射、回源配置、快速响应策略三个层面进行拆解。

一、浏览器预检请求与Access-Control-Request-Headers的生成机制
Access-Control-Request-Headers 是 CORS 预检请求的核心头之一。浏览器只会在非简单请求场景自动携带它,比如请求使用了自定义请求头、PUT 或 DELETE 方法,或者 Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain 之一。预检 OPTIONS 请求不会直接执行业务逻辑,而是先询问服务器是否允许这些方法和头。服务器必须在响应中返回 Access-Control-Allow-Headers,且内容必须覆盖请求中列出的每一项,否则浏览器会阻断后续真实请求。
在 Nginx 反向代理架构中,预检请求首先进入 Nginx。Nginx 本身的日志默认使用 combined 格式,通常只包含远程地址、时间、请求行、状态码、Referer 和 User-Agent 等字段,并不会展示 Access-Control-Request-Headers。如果开发者只看默认 access.log,就很难判断这个头是否已经到达 Nginx。更关键的是,后面回源到 Node.js、Java、Go 等上游服务时,如果 Nginx 在 location 块中直接拦截 OPTIONS 请求,预检根本不会进入业务应用,后端日志当然看不到任何内容。理解这些机制是排查跨域预检失败的前提。
二、Nginx 日志变量映射与自定义 log_format
Nginx 为请求头提供了统一的变量映射规则:先把请求头名称中的连字符替换成下划线,再全部转为小写,最后加上 http_ 前缀。于是 Access-Control-Request-Headers 对应的变量就是 $http_access_control_request_headers。这个变量在请求头不存在时值为空字符串,在日志中通常显示为 -。同理,Access-Control-Request-Method 对应 $http_access_control_request_method 变量。
要在日志中记录该头,需要自定义 log_format。配置可以放在 http 块中,然后在 access_log 指令中引用。以下配置增加了一个 cors_debug 格式,方便同时观察预检方法和请求头:
http {
log_format cors_debug '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" '
'preflight_method=$http_access_control_request_method '
'preflight_headers=$http_access_control_request_headers';
access_log /var/log/nginx/access.log cors_debug;
}
配置完成后,执行 nginx -s reload 重新加载。当浏览器发起预检时,日志每行末尾就会出现 preflight_method 和 preflight_headers 字段。如果 preflight_headers 有值,说明 Nginx 确实收到了该请求头;如果一直是 -,则需要检查前端是否真的发出预检,或者请求是否被更上层代理拦截。这种日志方案不改变转发行为,对生产环境影响很小,适合临时排查或长期开启。
三、回源透传与 OPTIONS 快速响应策略
在默认配置下,Nginx 会把原始请求头转发给上游服务,包括 Access-Control-Request-Headers。如果没有显式覆盖,回源链路通常不会丢失这个头。但实际项目中,很多团队会在 location 块里直接拦下 OPTIONS 请求,统一返回 204 或 200,这样就避免了预检请求打到后端。这种方案可以减少上游压力,但要求 Nginx 必须返回正确的 CORS 响应头,否则会引发布置了多个服务的跨域配置不一致。
以下示例展示了由 Nginx 直接响应预检的做法,其中 Access-Control-Allow-Headers 的值直接取自请求中的 $http_access_control_request_headers:
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS always;
add_header Access-Control-Allow-Headers $http_access_control_request_headers always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
proxy_pass http://backend_upstream;
}
如果仍然希望把预检请求回源到后端,由后端框架处理 CORS,那么一般不需要额外设置,默认透传即可。但有些安全加固配置会通过 proxy_set_header 覆盖或清理请求头,这时可以显式把该头传给上游:
location /api/ {
proxy_pass http://backend_upstream;
proxy_set_header Access-Control-Request-Headers $http_access_control_request_headers;
proxy_set_header Access-Control-Request-Method $http_access_control_request_method;
}
需要注意,如果变量为空,这种写法会让上游收到一个空值的请求头。是否可接受取决于后端框架的容错能力。更稳妥的做法是保持默认转发,并通过日志确认该头在到达 Nginx 时就是完整的。只有当上游明确反馈收不到该头时,才需要显式设置。回源配置的核心不是无脑加 proxy_set_header,而是先通过日志定位请求头在哪个环节消失。
四、用 curl 验证预检请求的日志与回源结果
curl 可以完全模拟浏览器的预检请求,很适合在修改 Nginx 配置后快速验证。下面命令中的反斜杠用于 bash 换行,执行时保留即可:
curl -I -X OPTIONS http://ipipp.com/api/users -H "Origin: https://app.ipipp.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: X-Custom-Token, Content-Type"
命令执行后,可以打开 Nginx 的 access.log 查看自定义字段。如果看到 preflight_headers=X-Custom-Token, Content-Type,说明日志配置生效且请求头已到达 Nginx。接着观察响应头,若返回了 Access-Control-Allow-Origin 和 Access-Control-Allow-Headers,并且内容与请求匹配,就代表 CORS 预检通过。若响应头缺失或值不完整,需要回头检查 add_header 是否加了 always,因为非 200 状态码下,部分响应头默认不会被发送。
排查过程中还要注意浏览器缓存。Access-Control-Max-Age 会让浏览器把预检结果缓存一段时间,如果修改配置后没有重新验证,可以先在浏览器开发者工具里勾选禁用缓存,或者换一个带新 Origin 的请求再测。另外,如果在 Nginx 前方还有 CDN 或负载均衡器,也需要确认它们是否把 Access-Control-Request-Headers 正确传递到 Nginx。日志方案可以把每一层是否收到该头变成可观测的事实,避免凭感觉猜测。
Nginx日志Access-Control-Request-Headers反向代理回源修改时间:2026-08-20 00:09:34