结论: 404 Not Found 表示服务器正常工作、也听懂了请求,但按这个路径找不到对应资源——问题出在"路径映射"而非"权限"或"程序崩溃"。九成以上的 404 集中在六类:root 或 alias 路径写错、缺少 try_files 或伪静态规则、Linux 大小写敏感、软链接失效、文件确实被删或没上传、CDN 缓存了旧规则。用 nginx -T 导出全部生效配置,再对照请求 URI 逐层比对,5 分钟即可定位。
404 是站长最熟悉也最容易误判的状态码。它不像 500 那样意味着程序崩了,也不像 403 那样是被拒绝,而是服务器翻遍了配置里指定的目录,没找到你要的那个文件。所以排查 404 的核心动作只有一个:搞清楚"请求 URI → 磁盘上真实文件"这条映射链在哪一环断了。本文以 Nginx 1.26 + Ubuntu 24.04 LTS 为主(截至 2026 年主流版本),覆盖 Apache 与常见 PHP/Java 框架的对应配置。
404 与其他"找不到类"状态码的边界
结论:先确认是硬 404、软 404 还是 403 误报,三者处理方式完全不同。
| 状态码 | 含义 | 典型触发 | 影响 |
|---|---|---|---|
| 404 Not Found | 资源不存在 | 路径写错、文件未上传 | 正常响应,搜索引擎会逐步删除索引 |
| 403 Forbidden | 资源存在但拒绝访问 | 权限/deny 规则/SELinux | 需按权限问题处理 |
| 410 Gone | 资源曾存在且已永久删除 | 主动下线内容 | 比 404 更快让搜索引擎下架 |
| 200 软 404 | 返回 200 但页面内容是"找不到" | SPA 未配 try_files、框架兜底页 | SEO 有害,搜索引擎会认为站点充斥空页 |
| 301/302 | 资源已搬家 | 目录调整未同步 | 应改用重定向而非让它 404 |
软 404 是最隐蔽的一种:用户看到"页面不存在",但 HTTP 状态码是 200。搜索引擎抓取时会把这些页面当作正常内容收录,导致站点出现大量低质量页面。用下面的命令可以直接判定:
# 只看状态码
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/old-page
# 同时看最终跳转后的状态码(跟随重定向)
curl -sL -o /dev/null -w "%{http_code} %{url_effective}\n" https://www.example.com/old-page
# 用 HEAD 请求看响应头
curl -I https://www.example.com/old-page404 成因对照表:9 类原因与首查动作
结论:按下表从上往下验证,命中即修,比凭经验猜要快得多。
| # | 成因 | 典型表现 | 验证命令 | 修复方式 | ||
|---|---|---|---|---|---|---|
| 1 | root/alias 路径写错 | 整站或某目录全 404 | `nginx -T \ | grep -n "root\ | alias"` | 改正路径后 nginx -t && reload |
| 2 | 缺 try_files(伪静态未配) | 首页正常,内页 404 | curl -I /article/123 | 加 try_files $uri $uri/ /index.php?$args | ||
| 3 | SPA history 模式未配 | 刷新子路由 404 | 直接访问 /user/1 | try_files $uri $uri/ /index.html | ||
| 4 | Linux 大小写敏感 | 本地 Windows 正常,上线后 404 | `ls \ | grep -i 文件名` | 统一改小写并同步引用 | |
| 5 | 软链接失效 | 某几个目录 404 | ls -l、readlink -f | 重建链接或改 root 指向真实路径 | ||
| 6 | 文件确实被删/没传上来 | 特定资源 404 | ls -l /path/to/file | 重新上传或恢复备份 | ||
| 7 | CDN 缓存了旧规则/旧 404 | 源站 200,CDN 404 | 直连源站对比 | 刷新 CDN 缓存、调整缓存规则 | ||
| 8 | rewrite 规则顺序错误 | 部分规则不生效 | nginx -T 看 rewrite 位置 | 调整顺序,注意 last/break | ||
| 9 | location 匹配优先级搞错 | 请求没进预期的块 | nginx -T 看全部 location | 用 =、^~ 明确优先级 |
location 匹配优先级:请求根本没进你以为的那个块
结论:Nginx 的 location 不是"谁写在前面谁生效",而是有固定优先级,搞错优先级是 404 的头号隐形原因。
优先级从高到低:
=精确匹配:location = /api完全相等才命中,命中后立即停止搜索。^~前缀匹配:命中后直接使用,不再检查任何正则。- **
~/~*正则匹配:按在配置文件中出现的先后顺序**检查,第一条命中即生效。 - 普通前缀匹配:取最长的那条。
server {
listen 80;
server_name www.example.com;
root /var/www/html;
# 1) 精确匹配:只有 / 这一个 URI 命中
location = / {
index index.html;
}
# 2) ^~ 前缀:以 /static/ 开头的全部走这里,且不检查下面的正则
location ^~ /static/ {
root /data/assets; # 实际查找 /data/assets/static/xxx
expires 30d;
}
# 3) 正则:按书写顺序,先 \.php$ 再图片类
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# 4) 兜底:最长前缀匹配,所有未被上面命中的请求
location / {
try_files $uri $uri/ /index.php?$args;
}
}一个典型事故:把 location ~ \.php$ 写在 location ^~ /static/ 之前,且站点把 PHP 文件放在 /static/ 下——由于 ^~ 优先级更高,请求根本不会进入 PHP 解析块,表现为 .php 文件被当作静态文件下载或直接 404。
root 与 alias 的区别:最容易写错的路径配置
结论:root 会把完整 URI 拼到路径后面,alias 则用指定路径替换 location 前缀,两者混用必然 404。
# root:最终路径 = root 值 + 完整 URI
# 请求 /static/logo.png → /var/www/html/static/logo.png
location /static/ {
root /var/www/html;
}
# alias:最终路径 = alias 值 + (URI 去掉 location 前缀后的部分)
# 请求 /static/logo.png → /data/assets/logo.png
location /static/ {
alias /data/assets/;
}注意事项:
alias用在正则location中时,必须使用捕获组,例如location ~ ^/static/(.+\.png)$ { alias /data/$1; }。alias目录末尾的斜杠要与location前缀一致(都带或都不带),否则拼接出的路径会少一层,直接 404。- 用
root时若location前缀与磁盘目录名不同名,拼接结果自然不存在——这就是"明明文件在,却 404"的经典场景。
验证实际查找路径的最快方法,是打开 Nginx 的 error.log 看 open() 后面的绝对路径:
sudo tail -f /var/log/nginx/error.log
# 404 时会看到:open() "/var/www/html/static/logo.png" failed (2: No such file or directory)
# 这一行直接告诉你 Nginx 实际去找的是哪个路径try_files 与 SPA history 模式:刷新子页面 404 的标准解法
结论:单页应用(SPA)使用 history 路由时,必须让所有未命中静态文件的请求回退到 index.html,否则刷新子路由必然 404。
server {
listen 80;
server_name app.example.com;
root /var/www/app;
index index.html;
# 静态资源正常走文件
location ~* \.(js|css|png|jpg|svg|woff2)$ {
expires 7d;
try_files $uri =404;
}
# 前端路由兜底:文件找不到就交给 index.html 由前端路由器接管
location / {
try_files $uri $uri/ /index.html;
}
# 后端 API 单独反代,不能被上面的兜底吃掉
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}try_files $uri $uri/ /index.html 的含义:先找与 URI 同名的文件,再找同名目录,都找不到就内部重定向到 /index.html。最后一个参数是兜底,必须存在,否则仍会 404。
Vue Router 与 React Router 的对应配置要点:
- Vue Router:
mode: 'history'(Vue 3 为createWebHistory()),构建后部署需上述 Nginx 配置。 - React Router:
BrowserRouter同理;若不想改服务端,可退回HashRouter(URL 带#,无需服务端配合)。 - 部署在子路径(如
/admin/)时,try_files兜底应写/admin/index.html,同时前端basename要一致。
伪静态配置:WordPress、Laravel、ThinkPHP 实战片段
结论:伪静态的本质是"把不存在的 URI 交给入口脚本处理",配置缺失就是整站内页 404。
WordPress(Nginx):
server {
root /var/www/wordpress;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# 禁止访问 PHP 文件的上传目录,防 Getshell
location ~* /uploads/.*\.php$ {
return 404;
}
}配置完成后还要在 WordPress 后台「设置 → 固定链接」中点击一次保存,让框架重写规则并写入 .htaccess(Apache)或刷新缓存。Apache 环境对应的 .htaccess 为:
# WordPress 根目录 .htaccess
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
Laravel 与 ThinkPHP:
# Laravel:入口在 public 目录,root 必须指到 public
root /var/www/laravel/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# ThinkPHP:老版本常用 rewrite 写法
location / {
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=$1 last;
}
}Apache 用户还需确认模块与 AllowOverride 已开启,否则 .htaccess 全部失效,表现为固定链接 404:
sudo a2enmod rewrite
sudo sed -i 's/AllowOverride None/AllowOverride All/' /etc/apache2/sites-available/000-default.conf
sudo systemctl restart apache2大小写敏感、软链接失效与 CDN 缓存的排查
结论:这三类问题在开发环境常被掩盖,上线后才暴露,且都不会在代码里留下痕迹。
# 1) 大小写:Linux 严格区分,Image.jpg 与 image.jpg 是两个文件
ls -l /var/www/html/img/ | grep -i "logo"
curl -I http://127.0.0.1/img/Logo.png # 对照实际文件名
# 2) 软链接失效:ls -l 中链接目标显示为红色闪烁即已断
ls -l /var/www/html/
readlink -f /var/www/html/uploads # 输出为空说明目标不存在
# Nginx 若配置了 disable_symlinks on; 也会拒绝访问符号链接
# 3) CDN 缓存:绕过 CDN 直连源站对比
curl -I -H "Host: www.example.com" http://源站IP/path
curl -I https://www.example.com/path针对三类问题的处理要点:
- 大小写:约定全站资源用小写命名,迁移上线前用
find . -name '[A-Z]'扫一遍;不要依赖 Windows/macOS 的不敏感文件系统。 - 软链接:容器化部署时宿主机路径与容器内路径不一致是断链主因;推荐直接挂载真实目录,而非链接。
- CDN 缓存:部分 CDN 默认会缓存 404 响应(默认缓存时长各家不同,截至 2026 年多为 10 秒到 5 分钟不等),源站修复后需主动刷新对应 URL;长期方案是在 CDN 控制台配置"不缓存 4xx"或把 404 缓存时长设为 0。
最后,nginx -T 是排查 404 的万能钥匙,它会把主配置与所有 include 进来的片段一次性打印:
# 导出全部生效配置并定位关键指令所在文件与行号
sudo nginx -T | grep -nE "root|alias|try_files|rewrite|location|include"
# 只看某个站点的完整配置
sudo nginx -T | sed -n '/server_name www.example.com/,/^}/p'
# 语法校验,改完必做
sudo nginx -t && sudo systemctl reload nginx
企业QQ咨询




