FDE·技术基础
基础目录
速查版 · 一屏看懂
①请求旅程 ②读懂monorepo ③数据库存东西 ④并发锁事务 ⑤前端为什么能跑 ⑥读懂AI代码
深入版 · 原理到能排障
①请求旅程 ②monorepo ③数据库 ④并发事务 ⑤前端渲染 ⑥读懂AI代码

深入 · 一次请求的完整旅程

深入版:不只讲「有哪几步」,而是讲清每一步到底在协商什么、失败了会怎样、你该怎么亲手验证。这一篇的落点是:以后线上出问题,你能用三条curl 命令定位到哪一层,而不是靠猜。

项目锚点

你服务器 124.222.108.196 上的 memberhandbook.cn,有三个天天见、说不清归属的现象:
①主站是 React 单页应用,84 个 URL给爬虫只返回 3120 字节空壳 HTML(正文全靠 JS 执行后才有)
② www.memberhandbook.cn 会 301 跳到非www(规范域名)
③ nginx 里有 try_files $uri $uri/ /index.html 这条兜底规则,/tool/ 上还多一条 try_files $uri.html $uri $uri/ /index.html

速查版讲的是「这三条现象分别归谁管」。这一篇往下钻一层:DNS 为什么改完要等、握手到底握什么、nginx 凭什么把请求交给 Node、返回 200 怎么可能是错的。

01

TCP 三次握手到底在握什么

先说清楚一件事:TCP 握手不传业务数据。它只解决一个问题——确认「我在」,顺便约定「我们以后用哪套规则说话」。

第 1 次 · 你 → 服务器
SYN:我准备开始了
浏览器发一个「我在吗,我要连你」的信号。这一次是客户端主动发起,所以必须由客户端先开口。
第 2 次 · 服务器 → 你
SYN + ACK:我在,你呢
服务器回一句:「我在,而且我记住了你的编号」。注意它把客户端的序号也确认了一遍——这才叫「双向确认」。
第 3 次 · 你 → 服务器
ACK:好,规则就是这一套
客户端再确认一次服务器的编号。到这一步双方都知道对方知道自己的编号,编号规则(谁发奇数、谁发偶数、丢包了怎么重传)正式生效,之后才进入传数据阶段。

为什么必须三次,两次不行吗

行不通,两次会留下一个致命漏洞:历史连接的垃圾包会被当成新连接。

举个真实场景:你 3 分钟前打开过网站,连接断了,路上留了一个包卡在网络里。3 分钟后你重新打开,这个迟到的旧包正好 arrive在新连接建立的那一刻。

握手次数发生什么后果
两次(假设可行)服务器发出 SYN+ACK 就认为连接建好了,开始等数据那个迟到的旧包被当成「客户端的第一批数据」收下→ 服务器把一个根本不存在的请求当成真的,浪费资源,甚至可能返回错误响应给一个没发请求的人
三次服务器要等到客户端的第三次确认才认为连接真建立迟到的旧包因为序号对不上,直接被丢弃,不会污染新连接
一句话:第3 次握手的唯一作用,是让服务器确认「对面那个说要连我的,是此刻真想连我的,不是三分钟前的幽灵」。
真实成本:三次握手要走 1 个 RTT(来回一趟)。国内到国内大概几十毫秒,到海外能到 200ms+。所以 HTTPS 首屏慢,大头往往不是服务器慢,是握手的来回次数多——HTTP/2 把多个请求合并到一条连接上,就是为了省掉后面那几十次握手。
02

TLS 握手在做什么

TCP 连接建好了,现在是一条明文的管道——路上任何人抓包都看得见你请求的 URL 和 Cookie。TLS 的任务就是在这条管道里装一把只有你和服务器有钥匙的锁。

大白话类比:共同商定一把钥匙

第1 步
服务器:这是我的「锁芯样本」
服务器把自己的公钥发过来。公钥可以随便看、随便转发,它只能锁,不能开。你拿到这个锁芯,寄给谁都不泄露。
第 2 步
你:那我造一把只有你我都有的钥匙
浏览器随机生成一把会话密钥(对称密钥),用服务器的公钥加密后发回去。路上被截获也解不开——攻击者没有私钥。
第 3 步
服务器:我解开,我们开始用对称加密
服务器用私钥解开,同时告诉你「用哪套加密算法规则」。之后两边都用对称加密传数据——比非对称快得多,因为不用每条消息都换一次钥匙。

那证书是干什么的

上面这套流程有个致命漏洞:如果我伪造一个服务器,把自己的公钥发给你,你加密后我用自己的私钥解开——你完全没察觉,数据全在我手里。

证书就是来解决这个漏洞的。证书里写着「这把公钥确实属于 memberhandbook.cn 这个域名」,并且由一个你信任的机构(CA,证书颁发机构)签了名。浏览器内置了一堆公认 CA 的公钥,只要签名对得上,就说明这把钥匙没被掉包。

所以「锁+ 证书」的分工是:锁(公钥/私钥)负责「只有你俩看得懂」,证书负责「对面确实是那个域名,不是冒充的」。小绿锁 = 这两件事都成立。

自签证书为什么浏览器一定报警

用openssl 自己签的证书,签名人是你自己,不是任何浏览器信任的 CA。浏览器会想:「任何人都能给自己签一张证书,我凭什么信?」——这就是「不受信任的证书 / NET::ERR_CERT_AUTHORITY_INVALID」。

场景浏览器表现本质
Let's Encrypt / 宝塔签发的证书小绿锁,正常访问签名人 CA 在浏览器信任列表里
自签证书红色警告页,要你手动点「继续」签名人不在信任列表,等于「无法验证是谁」
证书过期警告页有效期过了,身份声明失效
域名不匹配警告页证书写的是 A 域名,你访问的是 B 域名
你踩过的坑(证书家族子站):memberhandbook.cn 家族子站的 Let's Encrypt 证书由宝塔签发在 /www/server/panel/vhost/cert/<子站>/,不在 certbot 的仓库里。所以你跑 certbot renew 管不到它→ 证书过期 → nginx 仍引用旧证 → HTTPS 握手直接失败(https 000),现象是「页面完全打不开」,不是「页面变慢」。修法是重新签发后 手动 cp 到宝塔的 cert 路径,nginx 才认。
03

HTTP 请求与响应的完整结构

HTTP 的朴素之处在于:它就是一大段纯文本,没有二进制协议那么难读。请求和响应都是「一堆行 + 一个空行 + 正文」。

一次真实形状的请求

GET /fde/kb/14-request-deep.html HTTP/1.1
Host: memberhandbook.cn
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cache-Control: max-age=0
<空行>
(GET 请求没有正文,这个空行之后什么都没有)

第 1 行是请求行,三段:方法 + 路径 + 协议版本。GET 是「我要东西」,POST 是「我给你东西」。

空行之后是请求头,每行一个 名字: 值。请求头是「补充信息」:我要什么格式、有没有身份、有没有缓存。

一次真实形状的响应

HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 18432
Cache-Control: no-cache
Last-Modified: Thu, 02 Oct 2026 03:20:11 GMT
ETag: "66b1f0a2-4810"
<空行>
<!doctype html>
<html lang="zh-CN">
<head><title>深入 · 一次请求的完整旅程 ...</title></head>
...
</html>

第 1 行是状态行:版本 + 状态码 + 一句人类可读的原因短语(OK/Not Found/Moved Permanently)。程序只看状态码,那句短语是给人看的。

五个必须认识的头

头大白话真实作用 / 你的项目里
Host我要访问的是哪个站点同一台 IP 上挂了主站和一堆子站,全靠这个头区分。你自己在服务器上用 curl http://127.0.0.1/不带 Host 会落到默认站点 → 误判成404「文件不存在」,这是多 vhost 环境的经典坑,必须写 -H 'Host: lumenx.memberhandbook.cn'
User-Agent我是什么客户端爬虫识别完全靠它。你做反向验证时默认 curl UA 不改,会拿到和真实浏览器不同的内容,误判「站点不给爬虫」。你主站预渲染那次的验证就是:curl 带 DeepSeekBot UA 应该同样 200,证明问题在 JS 渲染不在 robots
Authorization我的身份凭证你 delimao-system 前端存了 JWT,发请求时带上:authorization: Bearer <token>。Bearer 的意思就是「持有者即可通行」,不校验是不是本人签发。这条是 httpBatchLink 的 headers() 里加的
Content-Type我发/我回的东西是什么格式请求里 application/json(tRPC 传的就是 JSON),响应里 text/html。最常见的 405 就是它对不上:一个只收 POST 的 API 被当成静态文件 GET 返回
Cache-Control这东西能不能缓存HTML 默认别缓存,静态资源(js/css/图)可以长缓存。它是「改了没生效」误判的常见来源——文件真改了,但浏览器/代理拿着旧缓存
红线:反向验证时不改 User-Agent,等于用假身份做实验。你之前做爬虫验证必须 curl -A "Mozilla/5.0...",原因就在这张表里。
04

状态码分类与重定向决策

类别含义典型码你该怎么做
1xx还在协商,没完100 Continue、101 升级一般看不到,工具会自动处理
2xx成了200拿到就够?不够,还得看内容对不对(见下方红线)
3xx换个地方301 /302 / 304 / 307决定「永久搬家」还是「临时借用」,选错会留后遗症
4xx你(客户端)错了400/ 401 / 403/ 404 / 405 / 422别重试,先查请求本身:路径、身份、方法
5xx我(服务器)错了500 / 502 / 503 / 504可以重试,但要查后端日志。502 = nginx 找到了后端但后端没回话;504 = 后端回话太慢

301 和 302 到底差在哪

1

301 Moved Permanently(永久搬家)

语义是「这个地址从此不再用了,以后都去新地址」。浏览器会把它记下来、缓存下来——下次你直接敲旧域名,浏览器可能根本不请求网络,直接跳新地址。这叫缓存型重定向。

2

302 Found(临时借用)

语义是「这次先你去那边,下一次我可能还给你别的答案」。浏览器不缓存它,每次都老实发请求问一次。

3

两者对SEO 的含义差别很大

搜索引擎把 301 当成「旧站把这页的权重全部移交到新页」,会把旧 URL 排出去换成新 URL。302 不会转移权重。所以换域名、合并页面必须用 301,用 302 等于告诉搜索引擎「我这儿只是临时有事」。

为什么规范域名(canonical)必须 301

你主站的真实问题:www.memberhandbook.cn 和 memberhandbook.cn 都能打开同一批页面。这在 SEO 里叫「重复内容 + 权重分散」——搜索引擎不知道该把排名给谁,两边都排不上。

server {
    listen 80;
    server_name www.memberhandbook.cn;
    # 规范域名:永久搬家,浏览器和搜索引擎都会记住
    return 301 https://memberhandbook.cn$request_uri;
}
# 反过来,非 www 那边所有请求都 301 过去 → 整个站只有一个「官方地址」
用 301 的理由一句话:「www 这个地址从此永久废弃了」这件事本身就是永久的,所以必须是 301。如果用 302,每次跳转都要多一次网络来回,搜索引擎还会短期认为你在摇摆、两套地址都算数。
红线(你真踩过):如果 80 端口的 server 级直接写 return 301 https://$host$request_uri;,certbot 的 HTTP-01 验证会被自己的 301 拦死——它要访问 /.well-known/acme-challenge/,请求被301 甩到死 https,证书永远签不出来。修法是先给 location ^~ /.well-known/acme-challenge/ 开个口子放行,再把剩下的 301 出去。
05

nginx 反代决策树

nginx 是门卫。一个请求进来,它按固定的优先级决定:读磁盘上的文件?还是转给 Node 进程?还是给404?

请求进来
  │
  ├─ ① 精确匹配location = /某个路径
  │     └─ 优先级最高,一旦命中就结束,别的规则再看
  │        (你给 /video/polish_r2v_prompt 单独开的就是这个)
  │
  ├─ ② 前缀匹配 location ^~ /前缀/
  │     ├─ 命中且带 ^~ → 立刻生效,不再往下匹配正则
  │     │   ⚠️ 好处:不会被后面的正则location 抢走
  │     │   ⚠️ 代价:同前缀的后端 API 也会被它吃掉
  │     │   (你踩过:POST /video/polish_prompt 被改写成 /files/video/polish_prompt → 静态目录只收 GET → 405)
  │     └─ 命中但不带 ^~ → 记下这个候选,继续试正则 location
  │
  ├─ ③ 正则匹配 location ~ /某个正则/
  │     └─ 命中就用它。nginx 按配置里出现的先后顺序试,
  │        第一个匹配的就赢(不是最长匹配)
  │
  ├─ ④ 普通前缀 location /前缀/(不带 ^~ 不带 ~)
  │     └─ 前面正则都没赢,才用这个(最长前缀优先)
  │
  └─ ⑤ 都没有 → 走 server 层的兜底
        │
        ├─ try_files $uri $uri/ /index.html
        │    ① $uri   当文件找  ──有就返回
        │    ② $uri/   当目录找,找里面index.html ──有就返回
        │    ③ /index.html ──都没有就返回它(SPA 兜底)
        │
        ├─ 都没有 → 404
        └─ 你也可以在最后写 return 200 /index.html 强制兜底

配置片段 A:静态目录(你项目 /fde/ 就是这一种)

location /fde/ {
    alias /www/wwwroot/memberhandbook.cn/fde/;
    # 关键:静态文件必须能被爬虫直接读到完整 HTML
    try_files $uri $uri/ =404;
    # ⚠️ 这里刻意不写 /index.html 兜底
    #因为 /fde/ 是真静态站,兜底会把「写错的路径」也返回 200,
    # 让 404 变成永远发现不了的假象
}

为什么这里不兜底:SPA 主站需要兜底(因为磁盘上真的没有 /bt/ 这类文件),但纯静态站不需要。写错 URL 就该给 404,这是给搜索引擎的诚实信号。

配置片段 B:反代到 Node(你 LumenX / H9 就是这一种)

location / {
    proxy_pass http://127.0.0.1:17177;
    # 反代五件套,少一个都会出诡异问题
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 3600s;   # AI 生成要等很久,超时必须放大
}
这五行在干什么不写会怎样
Host $hostNode 收到的是 127.0.0.1:17177,拼出来的绝对地址全错,回调链接、重定向全指向本地
X-Real-IP日志里所有请求 IP 都变成 127.0.0.1,你看不出谁在调,排查问题直接瞎
X-Forwarded-For丢失真实来源链,多层代理时无法定位
X-Forwarded-Proto后端不知道原请求是 https,拼出来的 URL 降级成 http,前端接口调用可能被浏览器拦(混合内容)
proxy_read_timeout默认 60 秒。你那边 AI 润色/视频生成要几分钟,60 秒一到 nginx 就把连接掐了,前端看到的是 504 而不是模型错误
反代的另一个坑:静态兜底会抢API。你 LumenX 有一组短路径兜底(/uploads/、/storyboard/、/video/、/audio/、/video_inputs/)转给后端的 /files/。它用 ^~,优先级高于给后端 API 用的正则 location,顺序无关。所以新增后端 API 一律别用这几个前缀命名。修法是加 location = /精确路径,优先级最高。
06

DNS 与缓存

DNS 就是电话簿:把域名翻译成 IP。你只需要认识一种记录——A。

记录类型作用你什么时候用
A域名 → IPv4 地址memberhandbook.cn → 124.222.108.196。你配站 99% 就是在配这一条
CNAME域名 → 另一个域名(别名)子域指向 CDN 或另一个主机名时用
AAAA域名 → IPv6 地址有 IPv6 才配
CNAME/NS/MX/TXT别名 / 权威服务器 / 邮件 / 各种验证站长验证的 TXT 记录走这一类
为什么改完 DNS 要等一段时间才生效?因为每一级 DNS 服务器都把查过的结果缓存下来了。你改了权威记录,只有等各级缓存里的旧记录过期,查询才会拿到新的。这解释了上线时最常见的一句话:「上线后别人打不开,我自己能开」——你本机的 DNS 缓存里是新值,或者你直接输的 IP 绕过了 DNS;别人手上还是旧值。
还有一个更隐蔽的缓存:浏览器自己的 DNS 缓存和 HSTS 缓存。你换服务器 IP 后,浏览器可能短时间内还往旧 IP 发请求。验证办法:curl 显式指定新 IP 并带上域名的 Host 头,这样能绕过 DNS 直接测到目标机器,用来区分「DNS 还没生效」还是「服务器本身有问题」。
# 绕过 DNS 直测某个 IP(区分是 DNS 问题还是服务器问题)
curl -I --resolve memberhandbook.cn:443:124.222.108.196 \
     https://memberhandbook.cn/fde/
#   ↑ --resolve 意思是「这次别查 DNS,直接用这个 IP,但域名的 Host 头照旧」
07

实测验证方法论(这一页最实用的部分)

线上出问题后,人工点开看不算验证。你看到的浏览器页面经过了缓存、重定向、JS 渲染多层加工。验证要程序化:看状态码 + 看字节数 + 看内容采样。

① 看状态码和响应头

curl -I https://memberhandbook.cn/fde/
# 输出举例:
# HTTP/2 200
# server: nginx
# content-type: text/html; charset=utf-8
# last-modified: Thu, 02 Oct 2026 03:20:11 GMT
# -I 只取响应头,不下载正文,快

② 模拟真实浏览器的 User-Agent

curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)\
 AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" \
      -I https://memberhandbook.cn/tool/abandoned
# ⚠️ 不加 -A 就是 curl 默认 UA,会拿到和真实浏览器不同的结果

③ 只取「状态码:字节数」

curl -s -o /dev/null -w "%{http_code}:%{size_download}\n" https://memberhandbook.cn/tool/abandoned
# 期望输出: 200:18432   (正常,有真实内容)
# 如果输出: 200:3120    ←这就是你主站那个空壳!字节数一下就暴露了

# 连类型一起看:
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://memberhandbook.cn/tool/abandoned
# 期望: 200 text/html

④ 对比不同 UA 的字节差异(判断爬虫有没有被区别对待)

# 同一 URL 换三种 UA 各测一次,把字节数并排比
for UA in "curl/8.0" "Mozilla/5.0 (compatible; DeepSeekBot/1.0)" "Mozilla/5.0 (compatible; Bytespider)"; do
  N=$(curl -s -A "$UA" -o /dev/null -w "%{http_code}:%{size_download}" https://memberhandbook.cn/tool/abandoned)
  echo "$UA  ->  $N"
done
# 判读:
#   三个字节数一样 → HTTP 层没拦爬虫,问题在页面是 JS 空壳
#   爬虫 UA 字节数明显小 → 服务器在按 UA 做区别返回,要去查 vhost
#   爬虫 UA 状态码非 200 → 才是真的被拦
这就是你那次为什么能下结论「不是 robots 的问题」。HTTP 层对三种 UA 返回一样的 200、一样的字节数,说明服务器没拦;那为什么 AI 搜索读不到?因为它读到的3120 字节里根本没有正文——那是 React 的空壳。顺着这条链才找到了真根因:需要预渲染,而不是去调 robots。

⑤ 顺手验证多 vhost 和跳转链

# 看完整的跳转链(-L 跟随,-v 看每次请求的头)
curl -IL http://www.memberhandbook.cn/fde/
# 期望: 301 → https://memberhandbook.cn/fde/ → 200
# 如果看到 302,或者干脆 200(www 也直接出内容)→ 规范域名没做对

# 服务器本机直测必须带 Host,否则落到默认站点误判 404
curl -H 'Host: lumenx.memberhandbook.cn' -I http://127.0.0.1/projects/
08

常见坑 / 红线

红线 1 · 只看到 200 就以为对了

200 只说明「服务器愿意给你一个响应」,不代表那个响应里有你要的东西。你 LumenX 整站挂着 Basic Auth,页面返回 200,但浏览器给你弹的是登录框——爬虫拿到的是登录页,不是正文。所以必须加一条:同时验证字节数和内容采样。

# 内容采样:确认拿到的确实是正文,不是登录页/错误页
curl -s https://memberhandbook.cn/tool/abandoned | grep -o '<title>[^<]*</title>'
# 如果输出是站点名(而不是这个工具页自己的标题)→ 就是空壳,需要预渲染
红线 2 · 301 和 302 混用

临时跳转用了 301,浏览器把跳转永久缓存了,你想改回来发现改不动——因为客户端已经不请求你了。规范域名、域名迁移用 301;登录后跳回、灰度切换、A/B 分流用 302 或 307。

红线 3 · 反向验证不改 User-Agent

拿 curl 默认 UA 去测,测的是「curl 看到什么」,不是「爬虫看到什么」。结论会错,方向会错,排查会白跑一轮。验证爬虫行为时,UA 必须换成对应爬虫的真名;验证用户行为时,用真实浏览器 UA。

红线 4 · 缓存导致「改了没生效」误判

你改完 HTML 刷新,页面还是老的。可能是浏览器缓存、CDN 缓存、或 Cache-Control 设成了长缓存。排查第一步永远是绕缓存验证:curl 直连(curl 不读浏览器缓存),确认服务器上新内容确实存在;如果 curl 能看到新的、浏览器看不到,那就是缓存问题,不是部署没成功。

✓

自查清单

  • ✓我能说清TCP 三次握手每一次在确认什么,以及为什么两次会收下历史垃圾包
  • ✓我能用「共同商定一把钥匙」解释 TLS,并说清证书存在的理由是防冒充
  • ✓我知道自签证书为什么报警,也知道宝塔签发的证书要 cp 到宝塔路径才生效
  • ✓我能写出一个完整 HTTP 请求和响应,并解释 Host / User-Agent / Authorization / Content-Type / Cache-Control 各自管什么
  • ✓我知道 301 会被浏览器缓存、302 不会,也知道规范域名只能用 301
  • ✓我能画出 nginx 决策树,说清 = > ^~ > 正则 > 普通前缀的优先级
  • ✓我知道 ^~ 抢API 会导致405,并会用 location = 修
  • ✓我能用 curl -s -o /dev/null -w "%{http_code}:%{size_download}" 一眼看出 SPA 空壳
  • ✓我知道 200 不等于对,要同时验字节数和内容采样
  • ✓我能解释「上线后别人打不开我自己能开」是各级 DNS 缓存造成的
面试怎么答

题目:讲讲用户访问一个网址到页面显示,发生了什么?

我按六步答,每步给一个可验证的证据,不空谈:

1. DNS——查 memberhandbook.cn 对应哪个 IP,拿到 124.222.108.196。这一步是查电话簿,还没开始传数据。改 DNS 后要等,是各级 DNS 都有缓存。

2. TCP 三次握手——建立连接、确认双方在线并约定传输规则。必须三次,因为两次的话服务器无法区分「对面是真想连,还是网络里迟到的历史包」。

3. TLS 握手——协商出只有双方知道的会话密钥。证书的作用是证明「这把钥匙的持有者真是这个域名」,因为不加证书的话任何人都能伪造一把公钥骗走你的数据。自签证书浏览器报警,就是因为签名人不在信任列表里。

4. HTTP 请求响应——请求行 + 请求头 + 空行 + 正文,响应状态行 + 响应头 + 正文。关键头是 Host(多站点靠它区分)、User-Agent(爬虫识别就靠它)、Authorization(我项目里 JWT Bearer 就走这个头)、Content-Type、Cache-Control。

5. nginx 决策——门卫。按 = 精确 → ^~ 前缀 → 正则 → 普通前缀的优先级选location,然后走 try_files 逐个尝试:文件 → 目录 → 兜底 index.html。我主站的 /tool/ 多一条 $uri.html,因为预渲染页存成了带 .html 的静态文件,URL 还是干净的。

6. 渲染——静态 HTML 直接显示;React SPA 则是下载空壳 + 一个大 JS,执行后才把内容填进 <div id="root">。

最后落到我们项目的真实对比上收尾:主站 84 个 URL 给爬虫只返回 3120 字节空壳,因为它是 JS 渲染;而 /fde/ 这些静态页 nginx 直接递出完整文字,不依赖 JS。所以收录差异不在 SEO 技巧,在架构——一个是图纸,一个是成品。验证也不靠人工点开,用 curl -s -o /dev/null -w "%{http_code}:%{size_download}" 取状态码和字节数,再换不同 User-Agent 对比,能程序化定位到问题在哪一层。

加分补充:如果对方继续问「你怎么验证」,就答curl -I 看头、curl -A 换 UA、curl -IL 看跳转链、--resolve 绕过 DNS——每一条都对应一个具体的排查场景。