Nginx 反向代理 + 全局跨域:那些看着通了但其实拦了的坑
前后端分离项目上线,前端一直报跨域,可 Nginx 里明明写了 Access-Control-Allow-Origin *。折腾一圈才发现,跨域配置里有几个"看着通了但其实拦了"的隐蔽坑。 最小可用配置 先把最小骨架搭起来——反代到后端 + 全局放开 CORS: server { listen 80; server_name your-domain.com; location / { proxy_pass http://backend.example.com:8002; 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; add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Requested-With,Token" always; if ($request_method = OPTIONS) { return 204; } } }这份能应付纯 API、无登录态的场景。要带 cookie 的话继续往下看,不然会直接被浏览器拦。 坑一:* 和 Allow-Credentials: true 不能共存 很多人从两份教程各抄一段,凑成: add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Credentials true always;浏览器规范明确禁止这种组合,直接跨域失败。 正确做法:把 * 换成 $http_origin 或白名单: add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true always; add_header Vary Origin always;坑二:Host 被写死 见过这种写法: proxy_set_header Host backend.example.com;后端如果基于 Host 做路由(Spring、Django 之类)或者生成回调 URL,就全乱套。改回: proxy_set_header Host $host;坑三:OPTIONS 只返回 204,没带 CORS 头 写了: if ($request_method = OPTIONS) { return 204; }有些浏览器要求预检响应本身也带跨域头,直接 return 204 就少一批头。解法是让 add_header 放在 if 之前——Nginx 的 add_header 会自动挂到最终响应上,只要不用 error_page 覆盖响应就行。 生产版本(推荐) 同时兼顾 cookie、白名单和预检: map $http_origin $cors_origin { default ""; "~^http://localhost:.*" $http_origin; "~^https://your-frontend\.com$" $http_origin; }server { listen 80; server_name your-domain.com; location / { proxy_pass http://backend.example.com:8002; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Requested-With,Token" always; add_header Vary Origin always; if ($request_method = OPTIONS) { return 204; } } }map 是精髓:预定义的域名才回响原 Origin,其它的会拿到空字符串,浏览器就自然拒绝。 一张表决定怎么选场景 推荐写法纯 API,无 cookie Allow-Origin *带 cookie / token map + $http_origin 白名单本地开发临时放开 Allow-Origin $http_origin一句话总结 CORS 卡住 90% 的时候都是这三件事:* 撞 Credentials、Host 写死、OPTIONS 没配好。抄配置前先想清楚要不要带 cookie。
Cloudflare Workers 开发指南:路由、反向代理、KV 存储和 TCP Socket
Cloudflare Workers 是部署在 Cloudflare 全球边缘节点上的无服务器运行环境,写 JavaScript/TypeScript 即可,无需管理服务器。 快速开始 npm create cloudflare@latest my-worker cd my-worker npm run dev # 本地调试 npm run deploy # 部署基础结构 export default { async fetch(request, env, ctx) { return new Response("hello world"); }, };路由处理 export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/api/user") { return Response.json({ name: "soulock" }); } return new Response("404", { status: 404 }); }, };获取 query 参数: const name = url.searchParams.get("name");获取请求体: const data = await request.json();反向代理 export default { async fetch(request) { const url = new URL(request.url); const target = "https://example.com"; const targetUrl = new URL(url.pathname + url.search, target); const headers = new Headers(request.headers); headers.set("Host", targetUrl.host); const resp = await fetch(targetUrl.toString(), { method: request.method, headers, body: request.body, redirect: "follow", }); return new Response(resp.body, { status: resp.status, headers: resp.headers, }); }, };流式返回(AI 接口必须用这种写法): // 正确:保持流式 return new Response(resp.body, resp);// 错误:会阻塞到完整响应 const text = await resp.text();CORS 处理 function corsHeaders() { return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", }; }export default { async fetch(request) { if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders() }); } const resp = await fetch("https://example.com"); return new Response(resp.body, { status: resp.status, headers: { ...Object.fromEntries(resp.headers), ...corsHeaders() }, }); }, };环境变量 # wrangler.toml [vars] API_KEY = "xxx"代码里用 env.API_KEY。生产环境推荐用 secret(不写入代码): wrangler secret put API_KEYKV 存储 [[kv_namespaces]] binding = "MY_KV" id = "xxxx"await env.MY_KV.put("key", "value"); const value = await env.MY_KV.get("key");D1 数据库(SQLite) wrangler d1 create mydb[[d1_databases]] binding = "DB" database_name = "mydb" database_id = "xxxx"const result = await env.DB.prepare("SELECT * FROM users").all();定时任务 export default { async scheduled(event, env, ctx) { // 定时执行的逻辑 }, };[triggers] crons = ["*/5 * * * *"]TCP Socket Workers 支持主动发起 TCP 连接(不能监听 TCP): import { connect } from "cloudflare:sockets";const socket = connect({ hostname: "example.com", port: 80 }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")); writer.releaseLock(); return new Response(socket.readable);常见用途:WebSocket → TCP 隧道,代理 Redis、PostgreSQL 等 TCP 协议。 Workers 不能替代大流量 TCP 服务(有连接数和执行时间限制),适合轻量中转。 推荐:Hono 框架 npm i honoimport { Hono } from "hono";const app = new Hono();app.get("/", (c) => c.text("hello")); app.get("/user", (c) => c.json({ name: "soulock" }));// 反向代理 app.all("*", async (c) => { const url = new URL(c.req.url); const target = "https://example.com" + url.pathname + url.search; const resp = await fetch(target, { method: c.req.method, headers: c.req.raw.headers, body: c.req.raw.body }); return new Response(resp.body, resp); });export default app;适合与不适合的场景适合 不适合API 网关 / 鉴权 大型 CPU 运算AI 接口中转 ffmpeg / 视频编码Webhook / Bot Puppeteer反向代理 本地文件读写短链接 / 边缘缓存 高并发大流量 VPNWorkers 没有 fs、child_process、net 等 Node.js 原生模块,运行在沙盒环境中。
Cloudflare Workers 入门:从 hello world 到 KV / D1
Cloudflare Workers 本质上是"边缘 JS/TS 函数"——写一段代码,部署到全球 300 多个节点,用户就近访问。适合做 API 代理、Webhook、AI 接口中转、鉴权网关,比自建 Nginx 或买 VPS 省事。 项目起步 一个命令拉起脚手架: npm create cloudflare@latest my-worker cd my-worker npm run dev # 本地起 wrangler dev npm run deploy # 部署到生产第一次部署会让你登录: npx wrangler login最小骨架 新版模块化写法: export default { async fetch(request, env, ctx) { return new Response("hello world"); }, };访问 https://<name>.<account>.workers.dev 就能看到。 路由分发 原生没有路由框架,URL 手动分: export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/api/user") { return Response.json({ name: "alice", age: 18 }); } if (url.pathname === "/health") { return new Response("ok"); } return new Response("Not Found", { status: 404 }); }, };要更完善的路由体验就上 Hono: import { Hono } from "hono"; const app = new Hono(); app.get("/api/user", (c) => c.json({ name: "alice" })); export default app;读取请求 // query string const url = new URL(request.url); const name = url.searchParams.get("name");// JSON body const data = await request.json();// 表单 const form = await request.formData();转发外部 API(最常见用法) export default { async fetch(request, env) { const resp = await fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` }, }); return new Response(resp.body, resp); }, };常见用途:藏 API Key、破跨域、加限流、加缓存、AI 接口中转。Key 千万别写死在代码里,走 wrangler secret。 KV:类 Redis 键值 wrangler.toml: [[kv_namespaces]] binding = "MY_KV" id = "xxxxxxxx"代码: await env.MY_KV.put("username", "alice"); const v = await env.MY_KV.get("username"); await env.MY_KV.delete("username");KV 是最终一致(全球复制有几秒延迟),做缓存 / 配置 / session 都很合适。 D1:SQLite [[d1_databases]] binding = "DB" database_name = "mydb" database_id = "xxxxxxxx"用法: const rows = await env.DB .prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .all();参数化查询是标配,别拼字符串——同样有 SQL 注入。 环境变量 vs Secret 普通变量写 wrangler.toml: [vars] APP_NAME = "my-app"敏感 key 用 secret,不进代码库: npx wrangler secret put OPENAI_API_KEY两种都通过 env.XXX 访问。 免费额度和局限免费 10 万次请求/天,付费 10 美元/月起 单请求 CPU 时间限制(免费 10ms,付费 50ms+,最高 5 分钟) 不能开 socket、不能持久保存本地文件 Node.js 内置模块要靠 nodejs_compat 兼容标志一句话总结 Workers = 全球边缘 JavaScript。npm create cloudflare@latest 起步,fetch 转发是 90% 用例,配上 KV/D1 就能扛住一个中等 API 项目。
Node 内置 SQLite 报 statement has been finalized 的原因
Node 22 开始有了内置的 node:sqlite,同步 API 用起来非常顺。但启动时挂: ExperimentalWarning: SQLite is an experimental feature Failed to start server Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) ... code: 'ERR_INVALID_STATE'这个错的字面意思很明确:你在一个已经 finalize() 过的 prepared statement 上继续调用 all() / get() / iterate()。 三种最常见的成因 1. 手动过早 finalize const stmt = db.prepare("SELECT * FROM devices"); try { return stmt.all(); } finally { stmt.finalize(); }看上去正确。但如果 stmt.all() 返回的是迭代器(iterate())而不是数组(all()),外层还在懒消费,finalize 一走就崩。用 all() 拿到具体数组的写法没这个问题。 2. 用了 using 自动 finalize(Node 22 的新语法) export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.iterate(); // ← 迭代器,出了作用域 stmt 就被 finalize 了 }using 借用了 TC39 的 Explicit Resource Management 提案,出作用域时会自动调 [Symbol.dispose](),也就是 finalize()。返回迭代器给外部继续用,外部一 next() 就炸。 规则:using + iterate() 不能混,要么用 all() 一次拿全部再返回,要么调用方也 using。 3. 模块级缓存了 stmt // 模块顶部 const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); }看着挺合理——避免每次 prepare 的开销。但代码其它地方可能:单元测试用 db.close() 关掉了数据库(stmt 一起 finalize) 有热重载/HMR 重新 import 了模块(旧 stmt 引用还留着) 显式调用了 readStmt.finalize() 想清理,然后忘了之后再走这个函数就 ERR_INVALID_STATE。 推荐写法 每次现 prepare,让 GC 管理: export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 立刻取完,不返回 iterator }现代 SQLite 的 prepare 开销很小,不需要为性能提前优化。 要缓存的话,配套复用而不是复用 + finalize: const cache = new Map<string, ReturnType<typeof db.prepare>>();function s(sql: string) { let stmt = cache.get(sql); if (!stmt) { stmt = db.prepare(sql); cache.set(sql, stmt); } return stmt; }// 用: s("SELECT * FROM device_states").all();不在缓存生命周期外手动 finalize。数据库 close 时统一 cache.clear()。 要用 using 就一次性拿完数据: export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 数组,可以安全跨作用域 }一句话总结 statement has been finalized = 你正在用一个死掉的 stmt。别返回迭代器给作用域外、别在模块级缓存又手动 finalize。改成"每次 prepare + all()"最省心。
Node.js SQLite statement has been finalized 错误:原因与修复
Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) { code: 'ERR_INVALID_STATE' }这个错误来自 Node.js 22+ 的内置 node:sqlite 模块,对已经调用了 finalize() 的 StatementSync 对象再次操作时抛出。 原因一:全局缓存 Statement 被意外 finalize // 错误写法:模块级共享 statement const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); // 若 readStmt 被 finalize 则崩溃 }如果在某处调用了 readStmt.finalize(),后续所有使用都会报错。 原因二:using 关键字自动释放 Node.js SQLite 支持 using 声明(Explicit Resource Management),离开作用域后自动调用 finalize(): // 错误:返回迭代器后 stmt 已被 finalize function getRows() { using stmt = db.prepare("SELECT * FROM logs"); return stmt.iterate(); // 离开函数后 stmt 自动 finalize,迭代器失效 }修复:每次使用时重新 prepare // 正确写法:每次都 prepare,不缓存 export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); const result = stmt.all(); stmt.finalize(); return result; }或者更简洁,不手动调用 finalize(Node.js 会在 GC 时自动释放): export function readDeviceStates() { return db.prepare("SELECT * FROM device_states").all(); }需要性能优化时:用对象包装 如果 prepare 开销较大,可以在模块初始化时准备,但确保 finalize 只在明确不再使用时调用: class DeviceRepository { private readStmt: StatementSync; constructor(private db: DatabaseSync) { this.readStmt = db.prepare("SELECT * FROM device_states"); } getAll() { return this.readStmt.all(); } close() { this.readStmt.finalize(); this.db.close(); } }生命周期由对象管理,close() 明确释放资源。 ExperimentalWarning 的处理 (node:2436) ExperimentalWarning: SQLite is an experimental featurenode:sqlite 在 Node.js 22 中是实验性功能,可以用以下方式消除警告: node --no-experimental-warnings server.js或在代码中: process.removeAllListeners('warning');生产环境建议关注 Node.js 版本更新,等待 SQLite 模块稳定。
