惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 聂微东
博客园 - 【当耐特】
Last Week in AI
Last Week in AI
量子位
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
B
Blog RSS Feed
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
GbyAI
GbyAI
Jina AI
Jina AI
宝玉的分享
宝玉的分享
腾讯CDC
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
D
Docker
博客园_首页
N
News and Events Feed by Topic
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Help Net Security
Help Net Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
F
Fortinet All Blogs
SecWiki News
SecWiki News
AI
AI
S
Schneier on Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
The Last Watchdog
The Last Watchdog
Martin Fowler
Martin Fowler
A
About on SuperTechFans
T
Troy Hunt's Blog
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
Hacker News: Ask HN
Hacker News: Ask HN
F
Full Disclosure
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
S
Security @ Cisco Blogs
Project Zero
Project Zero
Simon Willison's Weblog
Simon Willison's Weblog
C
Cyber Attacks, Cyber Crime and Cyber Security
J
Java Code Geeks
N
News | PayPal Newsroom
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
H
Hacker News: Front Page

博客园 - 一名程序媛呀

Anki插件开发必知必会:钩子函数与右键菜单定制 httpx 传参总报错?这次把 GET、POST、文件上传到响应处理的坑给你一次填平 从卡顿到丝滑:FastAPI 调用外部 API 的正确姿势(httpx 实战) 还在 XHR、Fetch 和 Axios 之间纠结?我踩过的坑,希望你一个都不用碰到 你的REST接口还在“过度投喂”数据吗?——FastAPI + GraphQL实战避坑指南 Uvicorn、Gunicorn 傻傻分不清?FastAPI 生产部署避坑指南 Termux里的二进制和脚本,到底怎么运行才不踩坑?Termux-service 保活妙招! 刚部署的 LibreTranslate 频频翻车?我掏出了 20 年前的 StarDict 词典,用 FastAPI 搭了个本地词典翻译 API 别再用网页翻译看源码了!你的私人翻译神器LibreTranslate,部署避坑指南来了 掏出手机就能搭个 WebDAV 同步服务器?这操作有点香 别只盯着GitBook了!这个文档神器让你的笔记秒变网站 写爬虫时用了代理还被封?Python 代理的那些隐藏坑,我替你踩明白了 FastAPI 身份验证总踩坑?这份 FastAPI Users “避坑指南”请收好 旧手机别扔!用 Termux 搭个私人云盘,比网盘香多了 你的FastAPI又在服务器上“跑不起来”了?来,今天咱把打包这件事彻底聊透 写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了 前端包管理咋选?我从npm叛逃到pnpm的血泪史(附避坑指南) 聊聊 fetch 使用中我踩过的那些坑和正确打开方式 FastApiAdmin 后端接口开发好了,前端管理界面怎么调用与显示? 给 FastApiAdmin 加个“会议纪要”模块,我把后端二次开发的坑踩了个遍 我用了FastApiAdmin后,连夜把踩过的坑都整理出来了 告别 Typora 后的新欢:我把所有笔记迁移到了 Obsidian 这个“第二大脑” 你的Agent API还在裸奔?从认证到沙箱,我用FastAPI搭了几道防线 让 FastAPI Agent 思考不阻塞:手把手教你实现异步任务与后台处理方案 让FastAPI Agent真正记住你:聊聊会话记忆与持久化存储的落地实践 FastAPI Agent 函数调用实战:我让 AI 学会了“自己动手查天气“ 初探:用 FastAPI 搭建你的第一个 AI Agent 接口 FastAPI 少有人提的实用技巧:把 Depends 依赖提到路由层,代码少写60% 用了loguru我才明白,Python日志还能这么写 FastAPI 后台任务:BackgroundTasks 的使用场景与注意事项 FastAPI配置管理避坑指南:从硬编码到 .env 与 pydantic_settings 类,连路由用法都给你捋清楚 FastAPI 文件上传避坑全指南:分块存盘、类型校验与安全兜底 FastAPI + Pydantic 模型终极实战手册:从能跑就行到固若金汤,这些技巧你一定用得上 FastAPI + SQLAlchemy 2.0 通用CRUD操作手册 —— 从同步到异步,一次讲透 FastAPI订单防超卖实战:从数据库锁到Saga分布式事务,这一篇给你理清了 FastAPI 生产环境避坑指南:用 Alembic 管理数据库迁移,别再手动改表结构了! FastAPI服务半夜又挂了?先别急着重启,查查你的数据库连接池“池子”是不是漏了 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 Vue 3 组合式 API 香是香,但从Vue2迁移时你可别像我当初一样踩进这 3 个深坑里 我用fastapi-scaff搭了个项目,两天工期缩到两小时,老板以为我开挂了 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! FastAPI自动生成的API文档太丑?我花了一晚上把它改成了客户愿意付费的样子 告别手写 API 胶水代码:FastAPI 与 Vue 的“契约自动机” OpenAPI 实战 FastAPI + Vue 前后端分离实战:我的项目结构“避坑指南” FastAPI + Celery 实战:异步任务里调用 Redis 和数据库的全解析,及生产级组织方案 FastAPI里玩转Redis和数据库的正确姿势,别让异步任务把你坑哭了! FastAPI + Celery 实战:异步任务的坑与解法,我帮你踩了一遍 FastAPI子应用挂载:别再让root_path坑你一夜 FastAPI项目半夜报警吵醒你?聊聊告警这事儿怎么搞! 别再数据线了!用FastAPI 5分钟搭个局域网文件+剪贴板神器 FastAPI单元测试实战:别等上线被喷才后悔,TestClient用对了真香! FastAPI状态共享秘籍:别再让中间件、依赖和路由“各自为政”了! FastAPI实战:WebSocket vs Socket.IO,这回真给我整明白了! 从0到1,FastAPI + PostgreSQL + Tortoise ORM 实战避坑指南 FastAPI + PostgreSQL 实战:给应用装上“缓存”和“日志”翅膀 FastAPI + PostgreSQL 实战:从入门到不踩坑,一次讲透
FastAPI 生产环境静态文件完全指南:从 /favicon.ico 404 到 HSTS 混合内容,一次全根治
一名程序媛呀 · 2026-05-02 · via 博客园 - 一名程序媛呀

你的 FastAPI 项目刚上线,还没来得及庆祝,就看到满屏的 /favicon.ico 404。是不是很眼熟?

我先坦白,当年我以为挂个静态文件简单得不能再简单,直到生产日志里这个404每天刷几百条,才意识到自己连“浏览器悄悄要了什么东西”都没搞清楚。

今天咱们就从这里撕开口子,把 FastAPI 静态文件挂载那点事儿聊透。

🎯 这篇文章能帮你解决什么

彻底根治 /favicon.ico 404,理解 app.mount 的真实工作原理,学会把用户上传的媒体文件与项目自有静态资源安全地分开放,不再因混合内容警告搞得焦头烂额。

📌 问题从哪儿来

每个现代浏览器在打开页面时,都会自动去请求 /favicon.ico 拿标签页的小图标。注意了,它是去根路径下要,不是 /static/favicon.ico

而咱们多数人第一次给 FastAPI 加静态文件,是这么写的:

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")

然后高高兴兴把 favicon.ico 扔进 static/ 文件夹,心想搞定。
结果呢? /static/favicon.ico 能正常访问,可根路径 /favicon.ico 仍然是 404。

🧠 app.mount 到底干了什么

FastAPI 底下是 Starlette,mount 的行为完全是前缀匹配。你挂载的是 /static,它就只认以 /static 开头的路径。
浏览器直接撞根路径的 /favicon.ico,Starlette 一瞅路由表里没有,也没有相应的挂载点,直接甩 404

换句话说,挂载点不是“全局共享目录”,而是一个子应用。你可以把 /static 想象成一个独立的小房子,门牌号写着“/static”,所以只有走这个门牌号的请求才进得来。

🔧 两种根治方案

方案一,单独给根路径挂一个专门放 favicon 的小目录。比如项目里建 root_public 目录,只放徽标文件:

import os
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
# 专门伺候根路径的 favicon.ico 等零散静态请求
app.mount("/", StaticFiles(directory="root_public"), name="root_public")

千万别偷懒,直接用 app.mount("/", StaticFiles(directory="static")) 暴力覆盖根路径——那样你所有路由都得和静态文件抢匹配,API 分分钟瘫痪。
专门分一个小目录,只放 favicon.ico、robots.txt 这类浏览器主动找的东西,是最稳妥的。

方案二,写一个简单路由直接返回文件:

from fastapi import FastAPI
from fastapi.responses import FileResponse

app = FastAPI()

@app.get("/favicon.ico")
async def favicon():
    return FileResponse("static/favicon.ico")

这个小路由干净利落,适合不太折腾的场景。

📂 多目录安全策略——用户上传的文件别乱放

解决了 favicon,咱们再往前想一步。用户上传的头像、附件,你敢直接跟项目自有的 JS、CSS 放一起吗?

之前出现过把用户上传的图片一股脑塞在 /static/uploads 里,部署时一不留神 git pull 把线上新文件冲掉了,而且安全扫描报了一堆“用户可控路径”警告🍵。

现在的标准做法是多层挂载 + 物理隔离

# 完美隔离!
app.mount("/static", StaticFiles(directory="static"), name="static")  # 我们自己写的代码,完全可信
app.mount("/media", StaticFiles(directory="/data/uploads"), name="media") # 用户上传的文件,可疑!

这样至少有三大好处:

- 项目源码目录(static)和用户生成内容(/media)物理隔离,部署不会相互覆盖。

- 可以给不同挂载点设置不同的缓存策略、权限和 Content-Security-Policy 头。

- 配合反向代理,/media 甚至可以指向独立的对象存储,不占应用磁盘 IO。

单是路径隔离还不够,再记两个保护灵魂的硬规矩

- 文件名只配当个参考

用户上传文件的名字,avatar.jpg 也好、malicious.exe 也罢,绝对不要直接用它来保存。必须用 UUID 等随机字符串重命名,把原始名字当个说明存在数据库里就好。
同时,必须强制限定上传文件的扩展名,比如只允许 .jpg、.png,这个检测要在后端进行,前端校验只是摆设。

- 文件内容发给专业选手检查

如果项目允许,引入杀毒引擎或专门的检测库扫描文件流,别只看后缀,坏人的坏水都在文件内容里。

这样一来,之前那个“用户可控路径”的警告,就从根源上被拆解了。现在,你心里应该踏实多了吧?😉

⚡ 再说个容易翻车的点:HSTS 和混合内容

HSTS(HTTP严格传输安全)。它通过一个响应头告诉浏览器:“这个网站,今后永远、只能、用 HTTPS 访问!别再用 HTTP 了,也别想着能允许例外。”
当你启用了 HSTS 强制 HTTPS,浏览器会对页面里任何 HTTP 资源发出混合内容警告,甚至直接拦截。

静态文件如果走挂载,协议是跟着应用来的,一般没事。
但有时候你在模板里硬编码了 HTTP 的外部 CDN 资源,或者用户上传后返回的 URL 是 http://,那麻烦就来了。

这就好比你搬进了一栋号称「24小时安保、全楼道监控」的高档公寓(这就是 HTTPS,安全的)。
结果(混合内容)公寓里混进了可疑人员,物业虽然很负责(HTTPS加密),但你家的 快递员、保洁阿姨、偶尔来串门的朋友,全都大摇大摆地走消防通道,没有门禁、没有登记(这就是 HTTP,不加密的)。

我现在的强迫症是:全站只允许相对路径或 // 形式的资源引用,上传文件入库的 URL 必须根据请求协议动态拼。 这不光是为了消灭报警,更是防止安全漏洞。

1. 在 HTML 模板里,直接用 url_for

这是最正宗、最不会出错的方式。它会自动根据你的挂载点生成路径,并且是相对路径。

<!-- 让 FastAPI 自己拼路径 -->
<link rel="icon" href="{{ url_for('static', path='favicon.ico') }}">
<img src="{{ url_for('static', path='images/logo.png') }}">
<script src="{{ url_for('static', path='js/app.js') }}"></script>

2. 如果你硬编码路径,就用协议相对URL

万一你必须在某个地方硬写路径,比如在一个独立的 .js 文件里定义图片地址,那么就用 // 开头。浏览器会自动把当前页面的协议(http 或 https)填上去。

// 好:自动适配协议
const logoUrl = '//cdn.example.com/logo.png';
const avatarUrl = '//www.example.com/media/default-avatar.png';

// 坏:写死协议,必遭混合内容警告
// const badUrl = 'http://cdn.example.com/logo.png';

✅ 用户上传内容,URL 必须根据上下文拼 https://

用户上传的图片、文件,最后落盘在你 /data/uploads 目录下,并通过你前面挂载的 /media 路径暴露出去。然后,你需要返回一个可访问的 URL 给前端。

标准的 Nginx/Caddy 反代后面

# nginx.conf
location / {
    proxy_pass http://127.0.0.1:8000;
    # ... 其他配置
    
    # 核心!告诉下游:上游用的什么协议
    proxy_set_header X-Forwarded-Proto $scheme;
}

然后在 FastAPI 里,我们写一个可靠的工具函数来拼出安全的 URL:

from fastapi import Request

def get_absolute_url(request: Request, path: str) -> str:
    """
    永远根据用户原始请求拼出正确协议的绝对路径 URL
    """
    # 1. 优先取反向代理传过来的原始协议,这是最准的
    proto = request.headers.get("X-Forwarded-Proto", "http")
    
    # 2. 用这个协议 + 当前请求的 host + 你的路径,拼出完整 URL
    #    例如:https://www.example.com/media/2023/avatar.jpg
    return f"{proto}://{request.headers['host']}{path}"

# 在你的上传接口里这样用
@app.post("/upload/")
async def upload_file(request: Request, file: UploadFile = File(...)):
    # ... 保存文件,得到相对于挂载点的路径,比如 /media/avatars/user123.jpg
    relative_path = f"/media/avatars/{saved_filename}"
    
    # 返回给前端的就是带 https 的绝对路径
    absolute_url = get_absolute_url(request, relative_path)
    
    return {"url": absolute_url}

💡 最后啰嗦一句

折腾这么一大圈,你会发现一个挺有意思的事:favicon.ico 404 只是个报信的小兵,它背后站着一整支需要认真对待的安全大军。

很多项目上线后,团队的选择是“哎呀反正不影响功能,监控静音算了”。
但这次咱没逃,从根路径挂载、多目录物理隔离,一路追到 HSTS 和混合内容,等于是把 FastAPI 静态文件这条链路从里到外捋了一遍。

你以为静态文件挂载只是个 Hello World,其实它藏着浏览器行为、应用路由、安全策略整整一套知识链。
把这一套理顺了,生产环境少掉的绝不止一个404报警,而是一整类因小失大的线上故障。

你问我最想让你记住什么?就三句:
 - 挂载点不是全局共享,浏览器要什么路径,你就得给什么路径。
 - 你和用户的东西,永远别放一个锅里搅,物理隔离是安全的第一块多米诺骨牌。
 - HTTPS 时代,凡是在模板里写死 http:// 的习惯,都是给未来的自己埋雷。

你可能会问,难道就不能一口气把 favicon 定死在 HTML 里?
当然可以,在 header 里加一句 <link rel="icon" href="/static/favicon.ico"> 也能绕开浏览器默认请求。
但经验告诉我,永远不要假设所有请求都是你页面发起的——爬虫、书签、各类工具都会直接请求 /favicon.ico,根路径解决方案才是根本。


觉得有用的话,顺手点个「赞」+「关注」一下吧,别让你的团队也在 favicon 和用户上传内容上踩同样的坑。收藏起来,下次部署前对着检查一遍,你就知道有多香😉。