关于 netlify-edge-functions
netlify-edge-functions 是面向开发者的 Netlify Edge Functions 开发技能,说明如何配置请求路由、处理中间件及访问请求上下文。内容还涵盖缓存、错误处理、环境变量和执行限制。
主要功能
- 01配置路径、模式和请求条件
- 02通过中间件修改源站响应
- 03使用重写和重定向
- 04读取 Cookie、地理位置和用户上下文
- 05配置响应缓存和错误处理
适用场景
- 01按路径或请求条件执行边缘逻辑
- 02修改博客页面的源站响应
- 03基于用户或地理信息限制访问
- 04为可复用响应配置缓存
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ netlify/ context-and-tools --skill netlify-edge-functions安装后在 Claude Code 中输入/netlify-edge-functions确认已加载。
详细介绍
Netlify Edge Functions
使用此表单
// netlify/edge-functions/hello.ts
import type { Config, Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
return new Response(`Hello from ${context.geo.country?.name ?? "somewhere"}`);
};
export const config: Config = { path: "/hello" };
- 一个文件对应一个函数:默认导出的处理程序,以及带有路由的
config导出。也可以在netlify.toml中声明路由。 Request和Response是全局对象。从@netlify/edge-functions导入Context和Config类型。- 使用
Netlify.env.get()读取环境变量。process.env仅适用于 Next.js 中间件。
请避免以下情况:
- 文件没有
config.path或pattern,且没有[[edge_functions]]条目。它会部署,但永远不会运行。 - 在
deno.json中使用导入映射。这不受支持。请使用单独的文件,并设置[functions] deno_import_map。 - 在
netlify.toml中设置变量。边缘函数无法读取这些变量。 - 将
Netlify.env.set()或Netlify.env.delete()用作持久化机制。它们只影响当前调用。 - 使用
fetch()请求当前路径所在的同一站点。请改用context.next()。
⚠️ 易踩的坑
- 没有路由 = 静默无操作。 边缘函数不会自动获得 URL。
path: "/*"会处理每个请求,包括静态资源。请缩小匹配范围,或添加excludedPath。- 缓存需要同时具备两部分: 配置中的
cache: "manual",以及返回的Response上的缓存标头。只设置一项不会缓存。 - 缓存函数会遮蔽静态文件。 在
/*上缓存的函数会返回/cat.png,而不是该文件。 - 返回
Response会终止调用链。 该路径的重定向规则不会运行。 - 同站点的
fetch()和URL重写会启动新的请求链。 匹配的边缘函数会再次运行。要访问同一路径上的静态资源或无服务器函数,请使用context.next()。 - 请求正文只能读取一次。 执行
await req.json()后,将new Request(req, { body })传给context.next()。 - 同名的
foo.ts和foo.js: TS 文件会被忽略,JS 文件会部署。 - 每个请求的 CPU 时间为 50 ms,包括
waitUntil工作。 - 启用 Split Testing 的站点不会执行边缘函数。
- 自定义标头和基本身份验证标头不适用于边缘函数。
- 不支持 HIPAA 合规托管。
netlify.app位于 Public Suffix List 中。 跨子域 Cookie 需要自定义域名。
文件
| 内容 | 位置 |
|---|---|
| 函数 | <base>/netlify/edge-functions/*.{js,ts,jsx,tsx} |
| 自定义目录 | [build] edge_functions = "my-dir"(相对于基础目录;请将其放在发布目录之外) |
| toml 中的名称 | 不含扩展名的文件名(auth.ts → function = "auth") |
| 导入映射 | 任意 JSON 文件,通过 [functions] deno_import_map = "./import_map.json" 引用 |
返回值
| 返回值 | 效果 |
|---|---|
Response | 发送给客户端;调用链结束 |
URL | 同站点重写,状态码为 200,地址栏保持不变 |
undefined / return; | 跳过此函数;调用链继续 |
端点
export default async () =>
new Response("Hello, World!", { headers: { "content-type": "text/html" } });
export const config = { path: "/api/hello" };
重定向 / 重写
import type { Context } from "@netlify/edge-functions";
export default async (req: Request, { cookies, geo }: Context) => {
if (geo.city === "Paris" && cookies.get("promo-code") === "15-for-followers") {
return Response.redirect(new URL("/subscriber-sale", req.url)); // redirect
// or: return new URL("/subscriber-sale", req.url); // rewrite (same-site only)
}
};
export const config = { path: "/sale" };
重写目标必须是同一站点。要访问其他站点,请使用 fetch。
中间件(修改源站响应)
import type { Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
if (new URL(request.url).searchParams.get("method") !== "transform") return;
const response = await context.next();
return new Response((await response.text()).toUpperCase(), response);
};
export const config = { path: "/blog/*" };
只有在需要响应正文时才调用 context.next()。如果只返回 undefined,调用链会自行继续。
条件请求
export default async (req: Request, { next }: Context) => {
const res = await next({ sendConditionalRequest: true });
if (res.status === 304) return res;
return new Response((await res.text()).toUpperCase(), res);
};
默认情况下,next() 会移除条件标头,因此始终返回完整响应。
读取正文后继续
export default async (req: Request, context: Context) => {
const body = await req.json();
if (!isValid(body.access_token)) return new Response("forbidden", { status: 403 });
return context.next(new Request(req, { body: JSON.stringify(body) }));
};
Netlify 用户门禁(私有 / 受团队登录保护的项目)
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const user = context.user; // undefined: public project, password protection, netlify dev
if (!user) return new Response("Forbidden", { status: 403 });
const allowed = (Netlify.env.get("ADMIN_USER_IDS") ?? "").split(",");
if (!allowed.includes(user.id)) return new Response("Forbidden", { status: 403 });
// user.email may be undefined, so use it for display only
};
export const config: Config = { path: "/admin/*" };
响应后的工作
export default async (req: Request, context: Context) => {
context.waitUntil(fetch("https://analytics.example.com/event", { method: "POST" }));
return new Response("ok");
};
waitUntil 不会阻塞响应,但仍计入 50 ms 的 CPU 时间限制。
流式 SSR(.tsx)
import React from "https://esm.sh/react";
import { renderToReadableStream } from "https://esm.sh/react-dom/server";
import type { Config, Context } from "@netlify/edge-functions";
export default async (req: Request, context: Context) => {
const stream = await renderToReadableStream(
<html><body><h1>Hello {context.geo.country?.name}</h1></body></html>
);
return new Response(stream, { headers: { "Content-Type": "text/html" } });
};
export const config: Config = { path: "/hello" };
Context
| 属性 | 说明 |
|---|---|
cookies.get(name) / .set(options) / .delete(name | options) | set 和 delete 使用 CookieStore 选项格式;它们作用于发出的响应 |
geo | city、country.{code,name}、subdivision.{code,name}、latitude、longitude、timezone、postalCode(格式各异) |
ip | 客户端 IP |
next(options?) / next(request, options?) | 链中下一个项目返回的 Promise<Response> |
params | path 中 URLPattern 的命名组(/pets/:name → { name })。对于查询字符串,请使用 request.url |
user | { id, email?, expiresAt } 或 undefined。这是 Netlify 账户,不是 Netlify Identity;对于 Identity,请使用 @netlify/identity 中的 getUser() |
waitUntil(promise) | 在发送响应后运行 |
requestId、account.id、server.region | — |
site | { id, name, url } |
deploy | { context, id, published, skewProtectionToken } |
Netlify 不会向请求添加客户端信息标头。请改为从 context 读取客户端信息。
Netlify.context 提供相同的对象,但仅在处理程序内部可用。在处理程序外部,它为 null。
Netlify.env:get(name)、has(name)、toObject(),以及仅限调用期间有效的 set 和 delete。若要持久更改变量,请使用 env API。
Web API:
console、atob、btoafetch、Request、Response、URL、File、BlobTextEncoder/TextDecoder(及其 Stream 变体)、Performance- Web Crypto:
randomUUID、getRandomValues、SubtleCrypto - WebSocket
setTimeout、clearTimeout、setInterval- Streams:
ReadableStream、WritableStream、TransformStream - URLPattern
模块:
- Node 内置模块:使用
node:前缀(import { randomBytes } from "node:crypto")。 - Deno 模块:通过 URL 或 import map 导入。
- npm 包(beta):按名称导入。包含原生二进制文件或运行时动态导入的包可能无法正常工作。
声明
Inline config
| 键 | 值 |
|---|---|
path | URLPattern 字符串或数组;必须以 / 开头 |
excludedPath | URLPattern 字符串或数组;必须以 / 开头 |
pattern / excludedPattern | 与 path / excludedPath 对应的正则表达式选项 |
method | HTTP 方法字符串或数组 |
header | { name: true | false | "regex" };名称不区分大小写;多值标头会与逗号拼接后的列表进行匹配 |
onError | "fail"(默认值)、"/custom-path" 或 "bypass" |
cache | "manual" |
export const config: Config = {
path: "/*",
excludedPath: ["/*.css", "/*.js", "/img/*"],
header: { "user-agent": "(iPhone|Android)" },
};
netlify-agent-category 请求标头会将任意 user-agent 映射到客户端类别。
netlify.toml
toml 格式支持 function、path、excludedPath、pattern、excludedPattern、[edge_functions.header] 和 cache。请在 inline 中设置 onError。需要明确指定同一路径上多个函数的执行顺序时,请使用 toml。
[[edge_functions]]
path = "/admin"
function = "auth"
[[edge_functions]]
path = "/admin"
function = "injector"
cache = "manual"
[[edge_functions]]
pattern = "/products/(.*)"
excludedPattern = "/products/things/(.*)"
function = "highlight"
[[edge_functions]]
path = "/*"
excludedPath = "/img/*"
function = "common"
[edge_functions.header]
x-required = true
x-forbidden = false
对 /admin 的请求会依次运行 auth、common,然后是 injector。
顺序
Netlify 会针对请求路径按以下顺序遍历两次。第一轮仅运行未配置缓存的函数。第二轮仅运行已缓存的函数。
- 在配置文件中声明的框架生成函数。
netlify.toml中从上到下的声明。- 框架和集成生成的 inline 声明。
- 按文件名的字母顺序排列的 inline 声明。
如果同一个函数同时在 toml 和 inline 中声明,配置会合并并按第 4 步处理;对于重复字段,以 inline 为准。
边缘函数执行完后,会运行重定向规则,除非某个函数已返回 Response。重定向到新路径后,会针对该路径重复整个流程。
声明在静态重写的目标路径上的边缘函数不会对重写后的请求运行。
响应缓存
import type { Config } from "@netlify/edge-functions";
export default async () =>
new Response("Hello world", { headers: { "cache-control": "public, s-maxage=3600" } });
export const config: Config = { path: "/hello", cache: "manual" };
- 必须在函数代码中设置标头。支持的标头:
Cache-Control、CDN-Cache-Control、Netlify-CDN-Cache-ControlExpires(会被max-age/s-maxage覆盖)Vary、Netlify-Vary
- 请参阅缓存概述。
- 同一部署上下文中的新部署会使缓存的响应失效。
- 命中缓存时会跳过调用,且不会计入调用限制。
- 缓存可重用的端点输出,例如 SSR。
- 不要缓存路由或个性化中间件,例如基于 Cookie 的产品排序。
- 本地开发没有缓存。本地会忽略缓存标头。
错误处理(onError)
| 值 | 效果 |
|---|---|
"fail"(默认) | 链停止;显示通用错误页面 |
"/unavailable" | 重写到同一站点路径;不会调用该路径上的边缘函数 |
"bypass" | 跳过发生故障的函数;链继续执行 |
对于关键逻辑,应采用失败关闭:内部身份验证使用默认值,面向客户的身份验证使用自定义路径。对于本地化等锦上添花的功能,应采用失败开放(bypass)。
环境变量
- 变量必须具有 Functions 作用域。
- Build 作用域的变量仅在构建时存在;如有需要,请在构建期间嵌入其值。
- 更改变量后需要重新部署。
- 部署信息(站点、区域、部署 ID)来自
context,而不是环境变量。
本地开发 / 部署 / 日志
- 运行
npm install netlify-cli -g,然后运行netlify dev。访问http://localhost:8888/<path>。- 更改会在下一个请求时生效。
- 使用
--edge-inspect或--edge-inspect-brk进行调试。 - 使用
--geo=mock(旧金山)模拟位置,或使用--geo=mock --country=DE。 - 在本地,
context.user未定义。
- 手动部署需要 CLI ≥ 12.2.8。部署是原子性的,Deploy Previews 和回滚也包含边缘函数。
- 日志:在 Netlify UI 中,前往 Cloud compute > Edge functions(
https://app.netlify.com/projects/{site_name}/edge-functions)。- 对于较早的部署:Deploys → 打开该部署 → Edge Functions。
console.log的输出会附带函数名称标签。- 保留时间 ≥ 24 小时(部分套餐为 7 天)。
- Enterprise 套餐提供 Log Drains。
限制
| 限制 | 值 |
|---|---|
| Bundle 大小 | 压缩后 20 MB |
| 内存(每组已部署函数) | 512 MB |
| 每个请求的 CPU 时间 | 50 ms(不包括 I/O 等待) |
| 响应标头超时 | 40 s |
- 预渲染不适用于由边缘函数提供服务的路径。
- 多个会生成边缘函数的框架插件可能会发生冲突。
文档:API · Declarations · Optional configuration · Limits · Examples
Netlify house rules(edge-functions)
这些是组织约定和从实践中总结的注意事项,并非文档事实;ctx-gen 会将它们合并到渲染后的技能中,且不会自动生成。 内容摘自之前手工编写的 netlify-edge-functions 技能;由技能维护者负责维护。
- 先查阅框架的适配器/参考文档:自定义边缘函数如果重复实现适配器生成的中间件,会导致冲突。只有当框架不会为该任务生成边缘函数时,才手动编写。
- 将
path的范围设得尽可能窄。path: "/*"会拦截每个请求——包括静态资源——给每个请求增加延迟,并为其计费一次边缘调用。 - 没有关联路由的边缘函数(没有 config 导出,也没有 netlify.toml 声明)仍会部署,但会悄无声息地从不运行:既不会出现构建错误,也不会有警告。当“我的边缘函数什么也没做”时,先检查路由。
- 根据工作负载特征选择边缘函数还是无服务器函数:边缘函数适用于低延迟的请求/响应处理、地理位置逻辑、身份验证检查/重定向和 A/B 个性化;无服务器函数适用于长时间运行的任务(最长 15 分钟)、依赖较重的 Node.js、数据库密集型操作、后台/定时任务,或内存需求超过 512 MB 的任务。
- 如果 config 中没有设置
cache: "manual",边缘响应上的缓存标头不会生效——二者必须同时设置,或者都不设置。除非函数也选择启用缓存,否则在返回的Response上设置Cache-Control不会生效。 - 说明声明处理顺序时,要说明两轮循环,而不只是排序:Netlify 会完整执行两次声明顺序——第一轮只运行未配置缓存的边缘函数,第二轮运行已配置缓存的边缘函数。只说“非缓存函数先于缓存函数”,却不说明循环过程,答案并不完整。
- 对于私有项目或启用了团队登录保护的项目,
context.user是发出请求的 Netlify 用户(包含id、email、expiresAt)。应使用它进行个人级检查和个性化处理,而不是另加一层登录;也绝不能从客户端发送的标头或 Cookie 中获取身份:Netlify 会在边缘函数运行前验证登录状态。所有示例都必须处理context.user为undefined的情况(公开项目、基本密码保护、netlify dev);email也可能是undefined,因此应依据id做判断。这是 Netlify 账户,不是 Netlify Identity 用户。