【Bug已解决】CORS Error on https://api.anthropic.com/v1/messages API with Reacrt.js 解决方案
一、现象长什么样
你在 React(原文拼写为 Reacrt.js)前端里直接用fetch调 Anthropic API:
fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "x-api-key": KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" }, body: JSON.stringify({ model: "claude-3-5-sonnet-latest", max_tokens: 256, messages: [...] }), });浏览器控制台报:
Access to fetch at 'https://api.anthropic.com/...' from origin 'http://localhost:3000' has been blocked by CORS policy;No 'Access-Control-Allow-Origin' header is present;- 预检
OPTIONS请求被拒; - 你换
axios、换XMLHttpRequest都一样,因为问题不在库,而在浏览器同源策略; - 用 Postman / curl / 后端能成功,只有浏览器前端失败。
一句话:api.anthropic.com没有返回Access-Control-Allow-Origin,浏览器出于同源策略禁止前端 JS 直接跨域调用它——Anthropic API 本就不设计给浏览器直连。
二、背景
CORS(跨源资源共享)是浏览器的安全机制:当前端页面(源 A)的 JS 去请求另一个源(源 B)的资源时,浏览器会要求 B 的响应带Access-Control-Allow-Origin头,否则拦截。
api.anthropic.com出于安全(主要是防止 API key 暴露在前端)不开启 CORS。原因很硬:
- 浏览器前端代码对用户可见,把
x-api-key写进前端等于把密钥公开; - 即使忽略密钥问题,服务端也没返回 CORS 头,浏览器照样拦;
- Anthropic 的设计是"密钥只在服务端使用",前端应通过你自己的后端去调用。
所以"在 React 里直连 Anthropic"从架构上就不被支持,CORS 只是表象,真正的问题是"密钥不该进浏览器"。
三、根因
根因是浏览器同源策略 + 服务端未开 CORS,且密钥不应暴露在前端:
React 前端 (http://localhost:3000) -> 浏览器发起跨域 fetch 到 api.anthropic.com -> 浏览器先做 CORS 预检(OPTIONS) -> 服务端未返回 Access-Control-Allow-Origin -> 浏览器拦截响应 -> CORS Error注意:即使你手动在请求里加Access-Control-Allow-Origin也没用——这个头是服务端响应才有效的,客户端设了被浏览器忽略。也不要试图在前端关 CORS(那是浏览器行为,前端改不了)。
四、最小可运行复现
下面演示"服务端是否有 CORS 头"的判断(用 Node 起一个本地后端中转,正确做法):
// 错误:浏览器前端直连(会被 CORS 拦) async function badCall() { const r = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "x-api-key": "sk-...", "anthropic-version": "2023-06-01" }, body: JSON.stringify({ model: "claude-3-5-sonnet-latest", max_tokens: 256, messages: [] }), }); return r.json(); // CORS Error } // 正确:前端只调自己的后端,密钥在后端的服务端 // 前端: async function goodCall(prompt) { const r = await fetch("/api/claude", { // 同源,无 CORS 问题 method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ prompt }), }); return r.json(); }后端(Node/Express)示例:
import express from "express"; import Anthropic from "@anthropic-ai/sdk"; const app = express(); app.use(express.json()); const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); // 密钥在服务端 app.post("/api/claude", async (req, res) => { const msg = await client.messages.create({ model: "claude-3-5-sonnet-latest", max_tokens: 256, messages: [{ role: "user", content: req.body.prompt }], }); res.json(msg); }); app.listen(3001);五、解决方案(第一层:最小直接修复)
最小修复是把 API 调用挪到服务端,前端只跟你自己的同源后端通信:
// React 前端 export async function askClaude(prompt) { const res = await fetch("/api/claude", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ prompt }), }); if (!res.ok) throw new Error(await res.text()); return res.json(); }后端用官方 SDK(密钥放环境变量,永不进前端),如上面 Express 示例。这样:
- 无 CORS 问题(前端与后端同源);
- API key 不暴露给用户;
- 还能在服务端做限流、审计、日志。
六、解决方案(第二层:结构化改进)
把"前端→服务端→Anthropic"的调用边界做成策略,集中约束"密钥只存在于服务端":
from dataclasses import dataclass import os from typing import Dict @dataclass(frozen=True) class AnthropicCorsReactPolicy: """前后端调用策略:密钥只在服务端,前端只调同源后端。 规则: - 前端禁止出现 x-api-key / ANTHROPIC_API_KEY - 前端只请求同源后端路径(如 /api/claude) - 后端用 SDK 调用 Anthropic,密钥取自环境变量 """ backend_route: str = "/api/claude" def frontend_call_shape(self) -> Dict[str, str]: # 前端应只发到同源后端,不带任何密钥头 return {"url": self.backend_route, "auth": "same-origin-session"} def backend_must_use_env_key(self) -> str: key = os.environ.get("ANTHROPIC_API_KEY", "") if not key: raise RuntimeError("后端必须从环境变量读取 ANTHROPIC_API_KEY,禁止硬编码") return "ok" def demo() -> None: policy = AnthropicCorsReactPolicy() print(policy.frontend_call_shape()) # 只到同源后端 # policy.backend_must_use_env_key() if __name__ == "__main__": demo()七、解决方案(第三层:断言 / CI 守护)
import os import pytest from your_module import AnthropicCorsReactPolicy def test_frontend_no_key_header(): policy = AnthropicCorsReactPolicy() shape = policy.frontend_call_shape() assert "x-api-key" not in str(shape) assert shape["url"].startswith("/api/") def test_backend_requires_env_key(monkeypatch): policy = AnthropicCorsReactPolicy() monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) with pytest.raises(RuntimeError): policy.backend_must_use_env_key() def test_backend_ok_with_env(monkeypatch): policy = AnthropicCorsReactPolicy() monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-x") assert policy.backend_must_use_env_key() == "ok" def test_frontend_targets_same_origin(): policy = AnthropicCorsReactPolicy() assert "anthropic.com" not in policy.frontend_call_shape()["url"]CI 里加一条:扫描前端打包产物,若发现sk-ant-或x-api-key字面量则构建失败,防止密钥泄露。
八、排查清单
- 是否在浏览器前端直连
api.anthropic.com?CORS 必拦,且密钥会泄露。 - 是否把
x-api-key写进了前端代码?立即移除,密钥只在服务端。 - 是否改用"前端→同源后端→Anthropic"的三段式?这是正解。
- 后端是否用官方 SDK + 环境变量密钥?不要用
fetch在前端带 key。 - 是否在前端强行关 CORS?那是浏览器行为,改不了,别浪费时间。
- CI 是否扫描前端产物防密钥硬编码?
九、小结
React 前端直连api.anthropic.com报 CORS Error,根因是 Anthropic 出于安全不开启 CORS(也本就不该浏览器直连),而把 API key 写进前端会直接泄露密钥。最小修复是改为"前端→同源后端→Anthropic":前端只调你自己的后端、密钥只在服务端用 SDK 调用;结构化做法是抽成AnthropicCorsReactPolicy,强制"前端无密钥、只走同源后端";最后用 pytest + CI 扫描守护"密钥不进前端、前端只请求同源路径",既解决 CORS 又守住安全底线。