不设 OLLAMA_ORIGINS 时,请求其实已经被服务端处理完了,
只是响应里少了 Access-Control-Allow-Origin 这个头,浏览器就不让页面读。
服务端日志一切正常,页面却报网络错误——这是最容易误判的一类故障。
上一节扩展装好了、模型也读到了。这一节讲它唯一一个真正的坑:跨域。 这一步卡住的人比前面所有步骤加起来都多。
一、跨域是什么,为什么本机也算跨域
浏览器的同源策略规定:协议、域名、端口三者全同,才算同源。只要有一项不同,就是跨域,
就要经过额外许可。
扩展页面的地址不是 http://,是 chrome-extension://abcdef...。协议不同、域名不同、端口也不同。
所以在 Ollama 眼里,这个扩展是一个彻底的外站。
这就解释了一个反直觉的现象:明明两边都在 127.0.0.1 上,明明没有走网络,照样被拦。
跨域判断的是「页面出身」,不是「距离远近」。
顺带一个对比:Cherry Studio 是桌面程序,不是网页,它不受同源策略约束,
所以上一节完全没遇到这个问题。这就是「桌面端能连、扩展连不上」的根本原因。
二、预检请求:浏览器先问一句
对于带 JSON 的 POST,浏览器会先发一个 OPTIONS 请求,相当于问一句「我能发这个请求吗」。
这叫预检。
服务端如果回了 Access-Control-Allow-Origin 这个头(简称 ACAO),而且值匹配,
浏览器才放行真正的 POST。没这个头,浏览器在 JS 拿到响应之前就把请求掐掉。
这里有个关键细节,也是最多人误解的地方:服务端其实已经把请求处理完了。它跑了模型、生成了回答、
写了响应,只是在响应头里少了一行。
所以现象是:Ollama 的日志里一切正常,页面控制台报 CORS 错误。两边的信息完全矛盾,
人被夹在中间不知道该信谁。信控制台。
Demo 里把两种情况都跑了一遍:不设 CORS 时,响应里没有 ACAO 头;
设了之后,多出 Access-Control-Allow-Origin: *。
两行输出只差一个头,但决定的是一次请求能不能完成。
三、怎么设:一个环境变量加一次重启
Windows:set OLLAMA_ORIGINS=*(当前终端窗口有效),或者在系统环境变量里加一条。
macOS/Linux:export OLLAMA_ORIGINS=*,或者写进启动脚本。
然后重启 Ollama 服务。 环境变量只在启动时读一次,改完不重启等于没改——这一步漏掉的人非常多。
生产环境别用 *,写具体来源(比如 chrome-extension://你的扩展ID)。
* 的意思是「任何网页都能调你的模型」,本机自用无所谓,但如果你在局域网里暴露了 11434,
这等于把模型开放给所有人。
还有一个隐性的点:如果 Ollama 装在别的机器上,要连它的地址也得配。跨域是浏览器端的限制,
跟服务端在哪儿无关——你在 A 机器开浏览器、连 B 机器的 Ollama,照样要 B 那边设 origins。
四、为什么这个错误看起来像「连不上」
浏览器把 CORS 失败报成网络错误。
控制台里可能写 Failed to fetch 或者 net::ERR_FAILED,看起来就像服务没起。
于是人就去做端口探测——结果端口通、/api/tags 也通、curl 发 POST 也正常。
因为这三件事都不经过浏览器的同源检查。
这个误导非常彻底:用命令行测,永远测不出跨域问题。命令行没有浏览器,没有同源策略。
所以判断方法要换一条:如果「命令行能通、页面不能」,
那就是跨域,不用再查别的。 这个判据简单而且几乎不会错。
五、动手 Demo:没设 origins 时响应少了 ACAO 头,服务端却已经处理完了
用同一个假服务,分别在「不带 CORS 头」和「带 CORS 头」两种配置下接收同一个请求,
打印状态码和 Access-Control-Allow-Origin 头的有无。再顺带列一遍三个配置项按出错概率的排序。
origin = u'chrome-extension://abcdefghijklmnop'
Mock.cors = False # 默认:不加 CORS 头
st, _b, hd, _d = post(url + u'/v1/chat/completions',
{u'model': u'x'}, headers={u'Origin': origin})
print(st, hd.get(u'Access-Control-Allow-Origin'))
Mock.cors = True # 设了 OLLAMA_ORIGINS
st2, _b2, hd2, _d2 = post(url + u'/v1/chat/completions',
{u'model': u'x'}, headers={u'Origin': origin})
print(st2, hd2.get(u'Access-Control-Allow-Origin'))
实跑输出:
--------------------------------------------------------------------
配置 Page Assist:卡住九成人的那一步是跨域
--------------------------------------------------------------------
扩展页面的出身不是 http,是 chrome-extension://,所以在 Ollama 眼里它是「外站」。
扩展的 Origin chrome-extension://abcdefghijklmnop
第一步,浏览器的预检请求(OPTIONS):
默认配置 状态 200,ACAO 头 没有
**没有这个头,浏览器会在 JS 拿到响应之前就把请求掐掉**,控制台报 CORS 错误。
注意:服务端其实已经处理完了,是浏览器不让页面读。
设了 OLLAMA_ORIGINS 状态 200,ACAO 头 *
有了这个头,请求才真正完成一个来回。
对应的动作只有一个:启动 Ollama 之前设一个环境变量。
Windows set OLLAMA_ORIGINS=*(当前窗口有效)
macOS/Linux export OLLAMA_ORIGINS=*
然后**重启 Ollama 服务**——环境变量只在启动时读一次,改完不重启等于没改。
三个配置项,按出错概率排:
① 跨域 扩展白屏、控制台报 CORS,就是这一条
② 地址端口 填 11434 还是别的端口,要和 Ollama 实际监听一致
③ 上下文长度 默认 2048,长文档要手动调,不然内容被截断还说不出原因
第一行:状态 200,ACAO 头没有。 注意状态码是 200——服务端从头到尾都正常工作了,
它只是没在响应头里加那一行。
第二行:状态 200,ACAO 头 *。 加了这一行,浏览器才允许页面读响应。
两行输出的差别只有一个头,但一次请求能不能完成就取决于它。
这段输出正好复刻了现实里最迷惑的场景:服务端一切正常、状态码 200、内容也对,可页面就是拿不到。
因为「拿不到」是浏览器干的,不是服务端干的。
后面那三个配置项按出错概率排了序。
跨域排第一不是因为它难,
是因为它的报错信息把人往错的方向引——报「连不上」,实际是「连上了但读不到」。
最后一条「上下文长度」排在第三,但它的隐蔽性最强:不报错、不白屏,只是模型「忘事」。
前两条会闹,第三条不闹,所以更难发现。
30 秒实验
- 把
Mock.cors在两个分支之间来回切,各跑一次,对比两次的完整响应头。 除了 ACAO,其他头一模一样。 - 把 Origin 改成
http://localhost:3000(一个普通网页),再跑一次。 结论不变——只要是跨源,就要这个头,跟是不是扩展无关。 - 把 ACAO 的值从
*改成chrome-extension://abc...(只允许那一个来源), 再用另一个 Origin 请求,看浏览器会不会拒。这就是生产环境的做法。
常见误区
- 「两边都在 127.0.0.1 上,不可能跨域」。跨域看的是页面出身(协议+域名+端口),不是物理距离。
chrome-extension://对http://就是跨源。 - 「用 curl 测通了就没问题」。 curl 没有浏览器,没有同源策略,永远测不出跨域问题。 命令行通、页面不通,就是跨域。
- 「改完环境变量就生效」。环境变量只在服务启动时读一次,必须重启 Ollama。 改完不重启,现象一模一样。
自测三问(点开对答案)
1. 为什么两边都在本机,扩展访问 Ollama 还是跨域? —— 同源策略看的是协议、域名、端口三者是否全同。 扩展页面是 `chrome-extension://`,Ollama 是 `http://127.0.0.1:11434`,协议和域名都不同, 所以是跨源。 2. 为什么「服务端日志正常、页面报网络错误」这种情况会出现? —— 因为服务端确实处理完了请求并返回了 200,只是响应头缺 ACAO。 浏览器在 JS 拿到响应之前把它拦掉了。**错在浏览器,不在服务端。** 3. 怎么快速判断一个「连不上」是不是跨域问题? —— 用命令行发同一个请求。命令行能通、页面不能,就是跨域。因为命令行不经过浏览器的同源检查。跨域过了,最后一步是验收。下一节用三项指标判断这套东西到底能不能用。
- ☐ (本节完,下一节 2.2.3 测试DeepSeek模型)