Update to Netlify
Snapshot Oct 7, 2026 · 00:02 UTC · version 1.6.0
Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.
Payment or plan references changed
Instruction wording changed from “Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() midd...” to “Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalizati...”. 204 additional added or edited lines are in the evidence.
Observed in published text. Live prices and checkout terms have not been verified by this change.
Product description
Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() midd...
Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalizati...
Skill instructions
Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() midd...
Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalizati...
Supporting files
[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":352},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686}]
[]
Compare saved observations
Download comparison JSONFull technical diff · 3 changed fields
changed /description
"Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() middleware pattern, geolocation, and when to choose edge vs serverless."
"Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one."
changed /included_files
[
{
"relative_path": "LICENSE.txt",
"size_in_bytes": 10776
},
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 352
},
{
"relative_path": "assets/netlify-small.svg",
"size_in_bytes": 1291
},
{
"relative_path": "assets/netlify.png",
"size_in_bytes": 2686
}
][]
changed /skill_md_contents
"---\nname: netlify-edge-functions\ndescription: Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() middleware pattern, geolocation, and when to choose edge vs serverless.\n---\n\n# Netlify Edge Functions\n\nEdge functions run on Netlify's globally distributed edge network (Deno runtime), providing low-latency responses close to users.\n\n## Syntax\n\n```typescript\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async (req: Request, context: Context) => {\n return new Response(\"Hello from the edge!\");\n};\n\nexport const config: Config = {\n path: \"/hello\",\n};\n```\n\nPlace files in `netlify/edge-functions/`. Uses `.ts`, `.js`, `.tsx`, or `.jsx` extensions.\n\n## Config Object\n\n```typescript\nexport const config: Config = {\n path: \"/api/*\", // URLPattern path(s)\n excludedPath: \"/api/public/*\", // Exclusions\n method: [\"GET\", \"POST\"], // HTTP methods\n onError: \"bypass\", // \"fail\" (default), \"bypass\", or \"/error-page\"\n cache: \"manual\", // Enable response caching\n};\n```\n\n## Middleware Pattern\n\nUse `context.next()` to invoke the next handler in the chain and optionally modify the response:\n\n```typescript\nexport default async (req: Request, context: Context) => {\n // Before: modify request or short-circuit\n if (!isAuthenticated(req)) {\n return new Response(\"Unauthorized\", { status: 401 });\n }\n\n // Continue to origin/next function\n const response = await context.next();\n\n // After: modify response\n response.headers.set(\"x-custom-header\", \"value\");\n return response;\n};\n```\n\nReturn `undefined` to pass through without modification:\n\n```typescript\nexport default async (req: Request, context: Context) => {\n if (!shouldHandle(req)) return; // continues to next handler\n return new Response(\"Handled\");\n};\n```\n\n## Geolocation and IP\n\n```typescript\nexport default async (req: Request, context: Context) => {\n const { city, country, subdivision, timezone } = context.geo;\n const ip = context.ip;\n\n if (country?.code === \"DE\") {\n return Response.redirect(new URL(\"/de\", req.url));\n }\n};\n```\n\nLocal dev with mocked geo: `netlify dev --geo=mock --country=US`\n\n## Environment Variables\n\nUse `Netlify.env` (not `process.env` or `Deno.env`):\n\n```typescript\nconst secret = Netlify.env.get(\"API_SECRET\");\n```\n\n## Module Support\n\n- **Node.js builtins**: `import { randomBytes } from \"node:crypto\";`\n- **npm packages**: Install via npm and import by name\n- **Deno modules**: URL imports (e.g., `import X from \"https://esm.sh/package\"`)\n\nFor URL imports, use an import map:\n\n```json\n// import_map.json\n{ \"imports\": { \"html-rewriter\": \"https://ghuc.cc/worker-tools/html-rewriter/index.ts\" } }\n```\n\n```toml\n# netlify.toml\n[functions]\n deno_import_map = \"./import_map.json\"\n```\n\n## When to Use Edge vs Serverless\n\n| Use Edge Functions for | Use Serverless Functions for |\n|---|---|\n| Low-latency responses | Long-running operations (up to 15 min) |\n| Request/response manipulation | Complex Node.js dependencies |\n| Geolocation-based logic | Database-heavy operations |\n| Auth checks and redirects | Background/scheduled tasks |\n| A/B testing, personalization | Tasks needing > 512 MB memory |\n\n## Limits\n\n| Resource | Limit |\n|---|---|\n| CPU time | 50 ms per request |\n| Memory | 512 MB per deployed set |\n| Response header timeout | 40 seconds |\n| Code size | 20 MB compressed |\n""---\nname: netlify-edge-functions\ndescription: Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one.\n---\n\n# Netlify Edge Functions\n\n## Modern syntax (reach for this)\n\nExport a default handler plus a `config` object. Import `Config`/`Context` types from `@netlify/edge-functions`; `Request`/`Response`/`URL` are global.\n\n```ts\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n return new Response(\"Hello world\");\n};\n\nexport const config: Config = {\n path: \"/test\",\n};\n```\n\n**Do not hand-write an edge function when your framework's adapter already generates middleware for the job** — duplicating it causes conflicts. Check the framework adapter/reference first.\n\n**Edge vs serverless:** use edge functions for low-latency request/response manipulation, geolocation logic, auth checks/redirects, and A/B personalization. Use serverless functions for long-running work (up to 15 min), heavy Node.js dependencies, database-heavy operations, background/scheduled tasks, or memory above 512 MB.\n\n## File location\n\n- Default directory: `YOUR_BASE_DIRECTORY/netlify/edge-functions`. Custom: `edge_functions` under `[build]` in `netlify.toml` (path relative to base directory).\n- Keep the directory **outside your publish directory** so source files aren't deployed.\n- Extensions: `.js`, `.ts`, `.jsx`, `.tsx` (`.jsx`/`.tsx` useful for SSR).\n- Same-name conflict: if `my-function.ts` and `my-function.js` both exist, the **TypeScript file is ignored** and the JavaScript one is deployed.\n\n## Routing — required, or the function silently never runs\n\n⚠️ **An edge function without a route (no `config` export and no `netlify.toml` declaration) still deploys but never runs — no build error, no warning.** When \"my edge function does nothing\", check the route first.\n\n⚠️ **Scope `path` narrowly.** `path: \"/*\"` intercepts every request including static assets, adding latency and billing an edge invocation for each one.\n\nEdge functions are **not** auto-assigned a URL route. Configure via inline `config` or `netlify.toml`.\n\n`path` is a `URLPattern` expression, must start with `/`, single string or array:\n\n```ts\nexport const config: Config = {\n path: [\"/\", \"/products/*\"],\n excludedPath: [\"/*.css\", \"/*.js\"],\n};\n```\n\nConfig properties: `path`, `excludedPath`, `pattern` (regex alternative to `path`), `excludedPattern`, `method`, `header`, `onError`, `cache`.\n\n### netlify.toml declaration\n\nUse `[[edge_functions]]` to declare multiple functions on one path and control order:\n\n```toml\n[[edge_functions]]\n path = \"/admin\"\n function = \"auth\"\n\n[[edge_functions]]\n path = \"/admin\"\n function = \"injector\"\n cache = \"manual\"\n\n[[edge_functions]]\n pattern = \"/products/(.*)\"\n excludedPattern = \"/products/things/(.*)\"\n function = \"highlight\"\n```\n\nProperties: `function`, `path`, `excludedPath`, `pattern`, `excludedPattern`, `header`, `cache`.\n\n**Merge precedence:** if the same function is declared both inline and in `netlify.toml`, configs merge and are treated as inline; inline wins duplicate fields.\n\n### Match by headers\n\n`header` keys are HTTP header names (case-insensitive); values are `true` (present), `false` (absent), or a string regex on the value. Multiple same-name values match against the comma-joined list.\n\n```ts\nexport const config: Config = {\n header: { \"x-required\": true, \"x-forbidden\": false, \"user-agent\": \"(iPhone|Android)\" },\n path: \"/*\",\n};\n```\n\n### Declaration processing order\n\nNetlify runs the whole declaration order **TWICE**: the first pass runs only edge functions **not** configured for caching; the second pass runs the ones **with** caching configured. Within that:\n\n1. Framework-generated functions declared in a config file.\n2. Your `netlify.toml` declarations (top-to-bottom order).\n3. Framework/integration-generated functions with inline config.\n4. Your inline declarations (**alphabetical by function file name**).\n\nTo control order across multiple functions on a path, prefer `netlify.toml` declarations over inline.\n\nAfter all functions run, Netlify evaluates redirect rules — unless a function returned a response and ended the chain. To customize order, use `netlify.toml`.\n\n**Order caveats:**\n- A returned response ends the chain; redirects for that path don't occur.\n- An edge function on the **target** of a static rewrite does **not** execute for rewritten requests.\n- `fetch()` for internal requests or returning a `URL` starts a **new request chain** and re-runs matching edge functions. Use `context.next()` to avoid re-running them.\n\n## Function signature & return values\n\nHandler receives `(request: Request, context: Context)`. Return one of:\n- a `Response` — delivered to the client; **ends the request chain** (declared redirects for that path don't run).\n- a `URL` — rewrite to a **same-site** URL with 200 status; address bar unchanged. Same-site only — for other sites use `fetch`.\n- `undefined` / empty `return;` — bypass this function, continue the chain.\n\nModify a response as middleware by awaiting `context.next()`:\n\n```ts\nimport type { Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n const url = new URL(request.url);\n if (url.searchParams.get(\"method\") !== \"transform\") return;\n\n const response = await context.next();\n const text = await response.text();\n return new Response(text.toUpperCase(), response);\n};\n```\n\nNetlify does **not** add headers to edge function requests — use `context` for client request info.\n\n## Common patterns\n\n**Redirect by geo + cookie:**\n```ts\nexport default async (req: Request, { cookies, geo }: Context) => {\n if (geo.city === \"Paris\" && cookies.get(\"promo-code\") === \"15-for-followers\") {\n return Response.redirect(new URL(\"/subscriber-sale\", req.url));\n }\n};\n```\n\n**Rewrite (same-site, 200):**\n```ts\nexport default async (request: Request, { geo }: Context) => {\n if (geo.city === \"Paris\") return new URL(\"/subscriber-sale\", request.url);\n};\n```\n\n**Read request body then continue** — a body can only be read once, so pass a new `Request` with an unread body:\n```ts\nexport default async (req: Request, context: Context) => {\n const body = await req.json();\n if (!isValid(body.access_token)) return new Response(\"forbidden\", { status: 403 });\n return context.next(new Request(req, { body: JSON.stringify(body) }));\n};\n```\n\n**Conditional request:**\n```ts\nexport default async (req: Request, { next }: Context) => {\n const res = await next({ sendConditionalRequest: true });\n if (res.status === 304) return res;\n const text = await res.text();\n return new Response(text.toUpperCase(), res);\n};\n```\n\n**SSR with React (`.tsx`):**\n```tsx\nimport React from \"https://esm.sh/react\";\nimport { renderToReadableStream } from \"https://esm.sh/react-dom/server\";\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async function handler(req: Request, context: Context) {\n const stream = await renderToReadableStream(\n <html><body><h1>Hello {context.geo.country?.name}</h1></body></html>\n );\n return new Response(stream, { status: 200, headers: { \"Content-Type\": \"text/html\" } });\n}\n\nexport const config: Config = { path: \"/hello\" };\n```\n\n## Context object\n\n- **`geo`** — `city`, `country.{code,name}`, `subdivision.{code,name}`, `latitude`, `longitude`, `timezone`, `postalCode`.\n- **`cookies`** — `get(name)`, `set(options)` (CookieStore.set format), `delete(name|options)`. Cross-subdomain cookies need a custom domain — impossible on `netlify.app` (Public Suffix List).\n- **`next(options?)`** / **`next(request, options?)`** — invoke the next item in the chain; returns a `Promise<Response>` you can modify. `options.sendConditionalRequest: true` for conditional requests. Only call `next` if you need the response body. Pass an explicit `Request` when you've read the body.\n- **`params`** — path params, e.g. path `/pets/:name` + request `/pets/winter` → `{name:\"winter\"}`. Query string: use `request.url`.\n- **`ip`** — client IP string.\n- **`requestId`** — Netlify request ID.\n- **`account.id`**, **`site.{id,name,url}`**, **`server.region`**, **`deploy.{context,id,published,skewProtectionToken}`**.\n- **`waitUntil(promise)`** — extend execution past the response (analytics, logs) without blocking it. Still subject to the CPU limit.\n\n**`Netlify` global:** `Netlify.context` (null outside the handler), `Netlify.env.{get,has,set,delete,toObject}`. `Netlify.env.set`/`delete` are **invocation-scoped only** — they do not persist env vars; use the Netlify env API endpoints.\n\n## Response caching\n\n⚠️ **Caching requires BOTH opting in AND setting headers — it's both or neither.** Setting `Cache-Control` on the returned `Response` does nothing without `cache: \"manual\"` in config, and vice versa. Default (either missing): every request invokes the function.\n\n1. Opt in: `cache: \"manual\"` (inline or `netlify.toml`).\n2. Set headers **inline in the function code** (not in `netlify.toml`):\n\n```ts\nimport type { Context, Config } from \"@netlify/edge-functions\";\n\nexport default async (req: Request, context: Context) => {\n return new Response(\"Hello world\", {\n headers: { \"cache-control\": \"public, s-maxage=3600\" },\n });\n};\n\nexport const config: Config = { cache: \"manual\", path: \"/hello\" };\n```\n\nSupported cache headers: `Cache-Control`, `CDN-Cache-Control`, `Netlify-CDN-Cache-Control`, `Expires` (overridden by `max-age`/`s-maxage`), `Vary`, `Netlify-Vary`. See https://docs.netlify.com/build/caching/caching-overview\n\n**Atomic deploys void the cache:** `s-maxage`/`max-age`/`Expires` are discarded by a new deploy in the same deploy context, even mid-lifetime.\n\n**When to cache:** endpoint responses reusable across clients (e.g. identical SSR HTML). **Do not cache** middleware, routing/transform logic, or per-client personalization.\n\n⚠️ **Caching functions always shadow static files.** A caching function on `/*` serves `/cat.png` instead of the static `cat.png`.\n\n## Error handling (`onError`, inline only)\n\n- **`fail`** (default) — serve a generic error page.\n- **`/YOUR_CUSTOM_PATH`** — rewrite to a same-site path (must start with `/`); served without invoking edge functions for that path.\n- **`bypass`** — skip the erroring function, continue the chain.\n\n```ts\nexport const config: Config = { path: \"/hello\", onError: \"/unavailable\" };\n```\n\nFail closed for critical logic (auth); fail open (`bypass`) for progressive enhancement (nice-to-have localization).\n\n## Environment variables\n\n- Set via UI/CLI/API; scope **must include Functions** to reach edge runtime.\n- **Env vars in `netlify.toml` are NOT available to edge functions.**\n- **Build-scope vars are NOT available at edge runtime** — only during the build step. Embed their values at build time if needed.\n- Changes require a **new build and deploy**; each deploy freezes values at deploy time.\n- Access at runtime with `Netlify.env.get(key)` / `Netlify.env.toObject()`.\n\n```ts\nexport default async (request: Request, context: Context) => {\n const value = Netlify.env.get(\"MY_IMPORTANT_VARIABLE\");\n return new Response(`Value: ${value}`);\n};\n```\n\nNext.js Middleware note: with Netlify Edge Functions for Middleware on Next.js, `process.env` also works.\n\n## Runtime & modules\n\nDeno-based. Import modules by:\n- **Node built-ins:** `import { randomBytes } from \"node:crypto\";`\n- **Deno/URL imports:** `import React from \"https://esm.sh/react\";`\n- **npm packages (beta):** `npm install` then import by name. ⚠️ Beta — packages using native binaries (Prisma) or runtime dynamic imports (cowsay) may fail.\n\n**Import maps** (module names instead of URLs) — use a separate import map file, declared in `netlify.toml`:\n\n```toml\n[functions]\n deno_import_map = \"./path/to/your/import_map.json\"\n```\n\nSupported Web APIs include `fetch`/`Request`/`Response`/`URL`/`File`/`Blob`, `console`, `atob`/`btoa`, `TextEncoder`/`TextDecoder` (+ stream variants), Web Crypto (`randomUUID`, `getRandomValues`, `SubtleCrypto`), WebSocket, timers, Streams API, URLPattern, `Performance`.\n\n## Local dev & deploy\n\n```bash\nnpm install netlify-cli -g\nnetlify dev # runs edge functions on local requests\n# visit http://localhost:8888/test\n```\n\n- Debug: `netlify dev` with `--edge-inspect` or `--edge-inspect-brk` (see https://cli.netlify.com/commands/dev/).\n- Geo mocking: `--geo=mock` (San Francisco) or `--geo=mock --country=XX`.\n- ⚠️ **No local caching** — cache headers are ignored in local testing.\n- Manual deploys require **Netlify CLI 12.2.8+** (older versions error).\n- Deploys are **atomic** — old deploys keep old behavior until you publish a new production deploy.\n\n**Monitor:** production logs at Netlify UI **Cloud compute > Edge functions**. Each `console.*` log includes the generating function name. Retention ≥ 24h (7 days on some plans). Log Drains on Enterprise.\n\n## Limits & feature gaps\n\n- **Code size:** 20 MB compressed (bundle max).\n- **Memory:** 512 MB per set of deployed edge functions.\n- **CPU time:** 50 ms per request (excludes wait time; `waitUntil` work still counts).\n- **Response header timeout:** 40 s.\n- Cached responses do **not** count toward invocations.\n- **Split Testing** enabled → edge functions do **not** run.\n- **Custom Headers** (incl. basic auth) do **not** apply to edge functions.\n- **Prerendering** does not apply to edge-served paths.\n- Rewrites are **same-site only** — use `fetch` for other/external sites.\n- Multiple framework plugins generating edge functions may collide.\n- **Not** supported under HIPAA-compliant hosting.\n\nSee the overview at https://docs.netlify.com/build/edge-functions/overview.md and the full example library at https://edge-functions-examples.netlify.app/\n\n<!-- system: agent-context/edge-functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (edge-functions)\n\nThese are org conventions and field-learned guardrails, not docs facts — they\nare merged into the rendered skill by ctx-gen and are never generated.\nExtracted from the previous hand-written netlify-edge-functions skill; owned\nby the skills maintainer.\n\n1. Check the framework's adapter/reference first: a custom edge function that\n duplicates adapter-generated middleware causes conflicts. Only hand-write\n an edge function when the framework doesn't already generate one for the\n job.\n2. Scope `path` narrowly. `path: \"/*\"` intercepts every request — including\n static assets — adding latency to each one and billing an edge invocation\n for it.\n3. An edge function without a route (no config export, no netlify.toml\n declaration) still deploys, but silently never runs: no build error, no\n warning. When \"my edge function does nothing\", check the route first.\n4. Choose edge vs serverless by workload shape: edge functions for low-latency\n request/response manipulation, geolocation logic, auth checks/redirects,\n and A/B personalization; serverless functions for long-running work (up to\n 15 min), heavy Node.js dependencies, database-heavy operations,\n background/scheduled tasks, or memory needs above 512 MB.\n5. Cache headers on an edge response do nothing without `cache: \"manual\"` in\n config — it's both or neither. Setting `Cache-Control` on the returned\n `Response` has no effect unless the function also opts in.\n6. When explaining declaration processing order, state the two-pass loop,\n not just the ordering: Netlify runs the whole declaration order TWICE —\n first pass runs only edge functions not configured for caching, second\n pass runs the ones with caching configured. \"Non-cached before cached\"\n without the loop framing is an incomplete answer.\n"SKILL.md line diff
--- before +++ after @@ -1,126 +1,331 @@ --- name: netlify-edge-functions -description: Guide for writing Netlify Edge Functions. Use when building middleware, geolocation-based logic, request/response manipulation, authentication checks, A/B testing, or any low-latency edge compute. Covers Deno runtime, context.next() middleware pattern, geolocation, and when to choose edge vs serverless. +description: Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like "add an edge function", "auth check at the edge", "redirect visitors by country", "A/B test with cookies", "rewrite requests", "personalize by geo", or "cache an edge response". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one. --- # Netlify Edge Functions -Edge functions run on Netlify's globally distributed edge network (Deno runtime), providing low-latency responses close to users. +## Modern syntax (reach for this) -## Syntax +Export a default handler plus a `config` object. Import `Config`/`Context` types from `@netlify/edge-functions`; `Request`/`Response`/`URL` are global. -```typescript +```ts import type { Config, Context } from "@netlify/edge-functions"; -export default async (req: Request, context: Context) => { - return new Response("Hello from the edge!"); +export default async (request: Request, context: Context) => { + return new Response("Hello world"); }; export const config: Config = { - path: "/hello", + path: "/test", }; ``` -Place files in `netlify/edge-functions/`. Uses `.ts`, `.js`, `.tsx`, or `.jsx` extensions. +**Do not hand-write an edge function when your framework's adapter already generates middleware for the job** — duplicating it causes conflicts. Check the framework adapter/reference first. + +**Edge vs serverless:** use edge functions for low-latency request/response manipulation, geolocation logic, auth checks/redirects, and A/B personalization. Use serverless functions for long-running work (up to 15 min), heavy Node.js dependencies, database-heavy operations, background/scheduled tasks, or memory above 512 MB. + +## File location + +- Default directory: `YOUR_BASE_DIRECTORY/netlify/edge-functions`. Custom: `edge_functions` under `[build]` in `netlify.toml` (path relative to base directory). +- Keep the directory **outside your publish directory** so source files aren't deployed. +- Extensions: `.js`, `.ts`, `.jsx`, `.tsx` (`.jsx`/`.tsx` useful for SSR). +- Same-name conflict: if `my-function.ts` and `my-function.js` both exist, the **TypeScript file is ignored** and the JavaScript one is deployed. + +## Routing — required, or the function silently never runs -## Config Object +⚠️ **An edge function without a route (no `config` export and no `netlify.toml` declaration) still deploys but never runs — no build error, no warning.** When "my edge function does nothing", check the route first. -```typescript +⚠️ **Scope `path` narrowly.** `path: "/*"` intercepts every request including static assets, adding latency and billing an edge invocation for each one. + +Edge functions are **not** auto-assigned a URL route. Configure via inline `config` or `netlify.toml`. + +`path` is a `URLPattern` expression, must start with `/`, single string or array: + +```ts export const config: Config = { - path: "/api/*", // URLPattern path(s) - excludedPath: "/api/public/*", // Exclusions - method: ["GET", "POST"], // HTTP methods - onError: "bypass", // "fail" (default), "bypass", or "/error-page" - cache: "manual", // Enable response caching + path: ["/", "/products/*"], + excludedPath: ["/*.css", "/*.js"], }; ``` -## Middleware Pattern +Config properties: `path`, `excludedPath`, `pattern` (regex alternative to `path`), `excludedPattern`, `method`, `header`, `onError`, `cache`. -Use `context.next()` to invoke the next handler in the chain and optionally modify the response: +### netlify.toml declaration -```typescript -export default async (req: Request, context: Context) => { - // Before: modify request or short-circuit - if (!isAuthenticated(req)) { - return new Response("Unauthorized", { status: 401 }); - } +Use `[[edge_functions]]` to declare multiple functions on one path and control order: - // Continue to origin/next function - const response = await context.next(); +```toml +[[edge_functions]] + path = "/admin" + function = "auth" + +[[edge_functions]] + path = "/admin" + function = "injector" + cache = "manual" + +[[edge_functions]] + pattern = "/products/(.*)" + excludedPattern = "/products/things/(.*)" + function = "highlight" +``` + +Properties: `function`, `path`, `excludedPath`, `pattern`, `excludedPattern`, `header`, `cache`. + +**Merge precedence:** if the same function is declared both inline and in `netlify.toml`, configs merge and are treated as inline; inline wins duplicate fields. + +### Match by headers + +`header` keys are HTTP header names (case-insensitive); values are `true` (present), `false` (absent), or a string regex on the value. Multiple same-name values match against the comma-joined list. - // After: modify response - response.headers.set("x-custom-header", "value"); - return response; +```ts +export const config: Config = { + header: { "x-required": true, "x-forbidden": false, "user-agent": "(iPhone|Android)" }, + path: "/*", }; ``` -Return `undefined` to pass through without modification: +### Declaration processing order -```typescript -export default async (req: Request, context: Context) => { - if (!shouldHandle(req)) return; // continues to next handler - return new Response("Handled"); +Netlify runs the whole declaration order **TWICE**: the first pass runs only edge functions **not** configured for caching; the second pass runs the ones **with** caching configured. Within that: + +1. Framework-generated functions declared in a config file. +2. Your `netlify.toml` declarations (top-to-bottom order). +3. Framework/integration-generated functions with inline config. +4. Your inline declarations (**alphabetical by function file name**). + +To control order across multiple functions on a path, prefer `netlify.toml` declarations over inline. + +After all functions run, Netlify evaluates redirect rules — unless a function returned a response and ended the chain. To customize order, use `netlify.toml`. + +**Order caveats:** +- A returned response ends the chain; redirects for that path don't occur. +- An edge function on the **target** of a static rewrite does **not** execute for rewritten requests. +- `fetch()` for internal requests or returning a `URL` starts a **new request chain** and re-runs matching edge functions. Use `context.next()` to avoid re-running them. + +## Function signature & return values + +Handler receives `(request: Request, context: Context)`. Return one of: +- a `Response` — delivered to the client; **ends the request chain** (declared redirects for that path don't run). +- a `URL` — rewrite to a **same-site** URL with 200 status; address bar unchanged. Same-site only — for other sites use `fetch`. +- `undefined` / empty `return;` — bypass this function, continue the chain. + +Modify a response as middleware by awaiting `context.next()`: + +```ts +import type { Context } from "@netlify/edge-functions"; + +export default async (request: Request, context: Context) => { + const url = new URL(request.url); + if (url.searchParams.get("method") !== "transform") return; + + const response = await context.next(); + const text = await response.text(); + return new Response(text.toUpperCase(), response); }; ``` -## Geolocation and IP +Netlify does **not** add headers to edge function requests — use `context` for client request info. -```typescript -export default async (req: Request, context: Context) => { - const { city, country, subdivision, timezone } = context.geo; - const ip = context.ip; +## Common patterns - if (country?.code === "DE") { - return Response.redirect(new URL("/de", req.url)); +**Redirect by geo + cookie:** +```ts +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)); } }; ``` -Local dev with mocked geo: `netlify dev --geo=mock --country=US` +**Rewrite (same-site, 200):** +```ts +export default async (request: Request, { geo }: Context) => { + if (geo.city === "Paris") return new URL("/subscriber-sale", request.url); +}; +``` + +**Read request body then continue** — a body can only be read once, so pass a new `Request` with an unread body: +```ts +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) })); +}; +``` + +**Conditional request:** +```ts +export default async (req: Request, { next }: Context) => { + const res = await next({ sendConditionalRequest: true }); + if (res.status === 304) return res; + const text = await res.text(); + return new Response(text.toUpperCase(), res); +}; +``` + +**SSR with React (`.tsx`):** +```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 function handler(req: Request, context: Context) { + const stream = await renderToReadableStream( + <html><body><h1>Hello {context.geo.country?.name}</h1></body></html> + ); + return new Response(stream, { status: 200, headers: { "Content-Type": "text/html" } }); +} + +export const config: Config = { path: "/hello" }; +``` + +## Context object + +- **`geo`** — `city`, `country.{code,name}`, `subdivision.{code,name}`, `latitude`, `longitude`, `timezone`, `postalCode`. +- **`cookies`** — `get(name)`, `set(options)` (CookieStore.set format), `delete(name|options)`. Cross-subdomain cookies need a custom domain — impossible on `netlify.app` (Public Suffix List). +- **`next(options?)`** / **`next(request, options?)`** — invoke the next item in the chain; returns a `Promise<Response>` you can modify. `options.sendConditionalRequest: true` for conditional requests. Only call `next` if you need the response body. Pass an explicit `Request` when you've read the body. +- **`params`** — path params, e.g. path `/pets/:name` + request `/pets/winter` → `{name:"winter"}`. Query string: use `request.url`. +- **`ip`** — client IP string. +- **`requestId`** — Netlify request ID. +- **`account.id`**, **`site.{id,name,url}`**, **`server.region`**, **`deploy.{context,id,published,skewProtectionToken}`**. +- **`waitUntil(promise)`** — extend execution past the response (analytics, logs) without blocking it. Still subject to the CPU limit. + +**`Netlify` global:** `Netlify.context` (null outside the handler), `Netlify.env.{get,has,set,delete,toObject}`. `Netlify.env.set`/`delete` are **invocation-scoped only** — they do not persist env vars; use the Netlify env API endpoints. + +## Response caching + +⚠️ **Caching requires BOTH opting in AND setting headers — it's both or neither.** Setting `Cache-Control` on the returned `Response` does nothing without `cache: "manual"` in config, and vice versa. Default (either missing): every request invokes the function. + +1. Opt in: `cache: "manual"` (inline or `netlify.toml`). +2. Set headers **inline in the function code** (not in `netlify.toml`): -## Environment Variables +```ts +import type { Context, Config } from "@netlify/edge-functions"; -Use `Netlify.env` (not `process.env` or `Deno.env`): +export default async (req: Request, context: Context) => { + return new Response("Hello world", { + headers: { "cache-control": "public, s-maxage=3600" }, + }); +}; -```typescript -const secret = Netlify.env.get("API_SECRET"); +export const config: Config = { cache: "manual", path: "/hello" }; ``` -## Module Support +Supported cache headers: `Cache-Control`, `CDN-Cache-Control`, `Netlify-CDN-Cache-Control`, `Expires` (overridden by `max-age`/`s-maxage`), `Vary`, `Netlify-Vary`. See https://docs.netlify.com/build/caching/caching-overview + +**Atomic deploys void the cache:** `s-maxage`/`max-age`/`Expires` are discarded by a new deploy in the same deploy context, even mid-lifetime. + +**When to cache:** endpoint responses reusable across clients (e.g. identical SSR HTML). **Do not cache** middleware, routing/transform logic, or per-client personalization. + +⚠️ **Caching functions always shadow static files.** A caching function on `/*` serves `/cat.png` instead of the static `cat.png`. -- **Node.js builtins**: `import { randomBytes } from "node:crypto";` -- **npm packages**: Install via npm and import by name -- **Deno modules**: URL imports (e.g., `import X from "https://esm.sh/package"`) +## Error handling (`onError`, inline only) -For URL imports, use an import map: +- **`fail`** (default) — serve a generic error page. +- **`/YOUR_CUSTOM_PATH`** — rewrite to a same-site path (must start with `/`); served without invoking edge functions for that path. +- **`bypass`** — skip the erroring function, continue the chain. -```json -// import_map.json -{ "imports": { "html-rewriter": "https://ghuc.cc/worker-tools/html-rewriter/index.ts" } } +```ts +export const config: Config = { path: "/hello", onError: "/unavailable" }; ``` +Fail closed for critical logic (auth); fail open (`bypass`) for progressive enhancement (nice-to-have localization). + +## Environment variables + +- Set via UI/CLI/API; scope **must include Functions** to reach edge runtime. +- **Env vars in `netlify.toml` are NOT available to edge functions.** +- **Build-scope vars are NOT available at edge runtime** — only during the build step. Embed their values at build time if needed. +- Changes require a **new build and deploy**; each deploy freezes values at deploy time. +- Access at runtime with `Netlify.env.get(key)` / `Netlify.env.toObject()`. + +```ts +export default async (request: Request, context: Context) => { + const value = Netlify.env.get("MY_IMPORTANT_VARIABLE"); + return new Response(`Value: ${value}`); +}; +``` + +Next.js Middleware note: with Netlify Edge Functions for Middleware on Next.js, `process.env` also works. + +## Runtime & modules + +Deno-based. Import modules by: +- **Node built-ins:** `import { randomBytes } from "node:crypto";` +- **Deno/URL imports:** `import React from "https://esm.sh/react";` +- **npm packages (beta):** `npm install` then import by name. ⚠️ Beta — packages using native binaries (Prisma) or runtime dynamic imports (cowsay) may fail. + +**Import maps** (module names instead of URLs) — use a separate import map file, declared in `netlify.toml`: + ```toml -# netlify.toml [functions] - deno_import_map = "./import_map.json" + deno_import_map = "./path/to/your/import_map.json" ``` -## When to Use Edge vs Serverless +Supported Web APIs include `fetch`/`Request`/`Response`/`URL`/`File`/`Blob`, `console`, `atob`/`btoa`, `TextEncoder`/`TextDecoder` (+ stream variants), Web Crypto (`randomUUID`, `getRandomValues`, `SubtleCrypto`), WebSocket, timers, Streams API, URLPattern, `Performance`. + +## Local dev & deploy + +```bash +npm install netlify-cli -g +netlify dev # runs edge functions on local requests +# visit http://localhost:8888/test +``` -| Use Edge Functions for | Use Serverless Functions for | -|---|---| -| Low-latency responses | Long-running operations (up to 15 min) | -| Request/response manipulation | Complex Node.js dependencies | -| Geolocation-based logic | Database-heavy operations | -| Auth checks and redirects | Background/scheduled tasks | -| A/B testing, personalization | Tasks needing > 512 MB memory | - -## Limits - -| Resource | Limit | -|---|---| -| CPU time | 50 ms per request | -| Memory | 512 MB per deployed set | -| Response header timeout | 40 seconds | -| Code size | 20 MB compressed | +- Debug: `netlify dev` with `--edge-inspect` or `--edge-inspect-brk` (see https://cli.netlify.com/commands/dev/). +- Geo mocking: `--geo=mock` (San Francisco) or `--geo=mock --country=XX`. +- ⚠️ **No local caching** — cache headers are ignored in local testing. +- Manual deploys require **Netlify CLI 12.2.8+** (older versions error). +- Deploys are **atomic** — old deploys keep old behavior until you publish a new production deploy. + +**Monitor:** production logs at Netlify UI **Cloud compute > Edge functions**. Each `console.*` log includes the generating function name. Retention ≥ 24h (7 days on some plans). Log Drains on Enterprise. + +## Limits & feature gaps + +- **Code size:** 20 MB compressed (bundle max). +- **Memory:** 512 MB per set of deployed edge functions. +- **CPU time:** 50 ms per request (excludes wait time; `waitUntil` work still counts). +- **Response header timeout:** 40 s. +- Cached responses do **not** count toward invocations. +- **Split Testing** enabled → edge functions do **not** run. +- **Custom Headers** (incl. basic auth) do **not** apply to edge functions. +- **Prerendering** does not apply to edge-served paths. +- Rewrites are **same-site only** — use `fetch` for other/external sites. +- Multiple framework plugins generating edge functions may collide. +- **Not** supported under HIPAA-compliant hosting. + +See the overview at https://docs.netlify.com/build/edge-functions/overview.md and the full example library at https://edge-functions-examples.netlify.app/ + +<!-- system: agent-context/edge-functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section --> +# Netlify house rules (edge-functions) + +These are org conventions and field-learned guardrails, not docs facts — they +are merged into the rendered skill by ctx-gen and are never generated. +Extracted from the previous hand-written netlify-edge-functions skill; owned +by the skills maintainer. + +1. Check the framework's adapter/reference first: a custom edge function that + duplicates adapter-generated middleware causes conflicts. Only hand-write + an edge function when the framework doesn't already generate one for the + job. +2. Scope `path` narrowly. `path: "/*"` intercepts every request — including + static assets — adding latency to each one and billing an edge invocation + for it. +3. An edge function without a route (no config export, no netlify.toml + declaration) still deploys, but silently never runs: no build error, no + warning. When "my edge function does nothing", check the route first. +4. Choose edge vs serverless by workload shape: edge functions for low-latency + request/response manipulation, geolocation logic, auth checks/redirects, + and A/B personalization; serverless functions for long-running work (up to + 15 min), heavy Node.js dependencies, database-heavy operations, + background/scheduled tasks, or memory needs above 512 MB. +5. Cache headers on an edge response do nothing without `cache: "manual"` in + config — it's both or neither. Setting `Cache-Control` on the returned + `Response` has no effect unless the function also opts in. +6. When explaining declaration processing order, state the two-pass loop, + not just the ordering: Netlify runs the whole declaration order TWICE — + first pass runs only edge functions not configured for caching, second + pass runs the ones with caching configured. "Non-cached before cached" + without the loop framing is an incomplete answer.
Full snapshot data
{
"description": "Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one.",
"included_files": [],
"name": "netlify-edge-functions",
"skill_md_contents": "---\nname: netlify-edge-functions\ndescription: Write and configure Netlify Edge Functions — TypeScript/JavaScript handlers running in a Deno runtime at the network edge. Use when adding auth middleware or auth redirects, geolocation or localization logic, A/B testing or personalization, request/response transforms (rewrites/redirects), or edge SSR to a Netlify site. Triggers on tasks like \"add an edge function\", \"auth check at the edge\", \"redirect visitors by country\", \"A/B test with cookies\", \"rewrite requests\", \"personalize by geo\", or \"cache an edge response\". Covers the config export, path routing, the Context object, response caching, environment variables, and edge-vs-serverless choices. Check the framework's adapter first — only hand-write an edge function when the framework doesn't already generate one.\n---\n\n# Netlify Edge Functions\n\n## Modern syntax (reach for this)\n\nExport a default handler plus a `config` object. Import `Config`/`Context` types from `@netlify/edge-functions`; `Request`/`Response`/`URL` are global.\n\n```ts\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n return new Response(\"Hello world\");\n};\n\nexport const config: Config = {\n path: \"/test\",\n};\n```\n\n**Do not hand-write an edge function when your framework's adapter already generates middleware for the job** — duplicating it causes conflicts. Check the framework adapter/reference first.\n\n**Edge vs serverless:** use edge functions for low-latency request/response manipulation, geolocation logic, auth checks/redirects, and A/B personalization. Use serverless functions for long-running work (up to 15 min), heavy Node.js dependencies, database-heavy operations, background/scheduled tasks, or memory above 512 MB.\n\n## File location\n\n- Default directory: `YOUR_BASE_DIRECTORY/netlify/edge-functions`. Custom: `edge_functions` under `[build]` in `netlify.toml` (path relative to base directory).\n- Keep the directory **outside your publish directory** so source files aren't deployed.\n- Extensions: `.js`, `.ts`, `.jsx`, `.tsx` (`.jsx`/`.tsx` useful for SSR).\n- Same-name conflict: if `my-function.ts` and `my-function.js` both exist, the **TypeScript file is ignored** and the JavaScript one is deployed.\n\n## Routing — required, or the function silently never runs\n\n⚠️ **An edge function without a route (no `config` export and no `netlify.toml` declaration) still deploys but never runs — no build error, no warning.** When \"my edge function does nothing\", check the route first.\n\n⚠️ **Scope `path` narrowly.** `path: \"/*\"` intercepts every request including static assets, adding latency and billing an edge invocation for each one.\n\nEdge functions are **not** auto-assigned a URL route. Configure via inline `config` or `netlify.toml`.\n\n`path` is a `URLPattern` expression, must start with `/`, single string or array:\n\n```ts\nexport const config: Config = {\n path: [\"/\", \"/products/*\"],\n excludedPath: [\"/*.css\", \"/*.js\"],\n};\n```\n\nConfig properties: `path`, `excludedPath`, `pattern` (regex alternative to `path`), `excludedPattern`, `method`, `header`, `onError`, `cache`.\n\n### netlify.toml declaration\n\nUse `[[edge_functions]]` to declare multiple functions on one path and control order:\n\n```toml\n[[edge_functions]]\n path = \"/admin\"\n function = \"auth\"\n\n[[edge_functions]]\n path = \"/admin\"\n function = \"injector\"\n cache = \"manual\"\n\n[[edge_functions]]\n pattern = \"/products/(.*)\"\n excludedPattern = \"/products/things/(.*)\"\n function = \"highlight\"\n```\n\nProperties: `function`, `path`, `excludedPath`, `pattern`, `excludedPattern`, `header`, `cache`.\n\n**Merge precedence:** if the same function is declared both inline and in `netlify.toml`, configs merge and are treated as inline; inline wins duplicate fields.\n\n### Match by headers\n\n`header` keys are HTTP header names (case-insensitive); values are `true` (present), `false` (absent), or a string regex on the value. Multiple same-name values match against the comma-joined list.\n\n```ts\nexport const config: Config = {\n header: { \"x-required\": true, \"x-forbidden\": false, \"user-agent\": \"(iPhone|Android)\" },\n path: \"/*\",\n};\n```\n\n### Declaration processing order\n\nNetlify runs the whole declaration order **TWICE**: the first pass runs only edge functions **not** configured for caching; the second pass runs the ones **with** caching configured. Within that:\n\n1. Framework-generated functions declared in a config file.\n2. Your `netlify.toml` declarations (top-to-bottom order).\n3. Framework/integration-generated functions with inline config.\n4. Your inline declarations (**alphabetical by function file name**).\n\nTo control order across multiple functions on a path, prefer `netlify.toml` declarations over inline.\n\nAfter all functions run, Netlify evaluates redirect rules — unless a function returned a response and ended the chain. To customize order, use `netlify.toml`.\n\n**Order caveats:**\n- A returned response ends the chain; redirects for that path don't occur.\n- An edge function on the **target** of a static rewrite does **not** execute for rewritten requests.\n- `fetch()` for internal requests or returning a `URL` starts a **new request chain** and re-runs matching edge functions. Use `context.next()` to avoid re-running them.\n\n## Function signature & return values\n\nHandler receives `(request: Request, context: Context)`. Return one of:\n- a `Response` — delivered to the client; **ends the request chain** (declared redirects for that path don't run).\n- a `URL` — rewrite to a **same-site** URL with 200 status; address bar unchanged. Same-site only — for other sites use `fetch`.\n- `undefined` / empty `return;` — bypass this function, continue the chain.\n\nModify a response as middleware by awaiting `context.next()`:\n\n```ts\nimport type { Context } from \"@netlify/edge-functions\";\n\nexport default async (request: Request, context: Context) => {\n const url = new URL(request.url);\n if (url.searchParams.get(\"method\") !== \"transform\") return;\n\n const response = await context.next();\n const text = await response.text();\n return new Response(text.toUpperCase(), response);\n};\n```\n\nNetlify does **not** add headers to edge function requests — use `context` for client request info.\n\n## Common patterns\n\n**Redirect by geo + cookie:**\n```ts\nexport default async (req: Request, { cookies, geo }: Context) => {\n if (geo.city === \"Paris\" && cookies.get(\"promo-code\") === \"15-for-followers\") {\n return Response.redirect(new URL(\"/subscriber-sale\", req.url));\n }\n};\n```\n\n**Rewrite (same-site, 200):**\n```ts\nexport default async (request: Request, { geo }: Context) => {\n if (geo.city === \"Paris\") return new URL(\"/subscriber-sale\", request.url);\n};\n```\n\n**Read request body then continue** — a body can only be read once, so pass a new `Request` with an unread body:\n```ts\nexport default async (req: Request, context: Context) => {\n const body = await req.json();\n if (!isValid(body.access_token)) return new Response(\"forbidden\", { status: 403 });\n return context.next(new Request(req, { body: JSON.stringify(body) }));\n};\n```\n\n**Conditional request:**\n```ts\nexport default async (req: Request, { next }: Context) => {\n const res = await next({ sendConditionalRequest: true });\n if (res.status === 304) return res;\n const text = await res.text();\n return new Response(text.toUpperCase(), res);\n};\n```\n\n**SSR with React (`.tsx`):**\n```tsx\nimport React from \"https://esm.sh/react\";\nimport { renderToReadableStream } from \"https://esm.sh/react-dom/server\";\nimport type { Config, Context } from \"@netlify/edge-functions\";\n\nexport default async function handler(req: Request, context: Context) {\n const stream = await renderToReadableStream(\n <html><body><h1>Hello {context.geo.country?.name}</h1></body></html>\n );\n return new Response(stream, { status: 200, headers: { \"Content-Type\": \"text/html\" } });\n}\n\nexport const config: Config = { path: \"/hello\" };\n```\n\n## Context object\n\n- **`geo`** — `city`, `country.{code,name}`, `subdivision.{code,name}`, `latitude`, `longitude`, `timezone`, `postalCode`.\n- **`cookies`** — `get(name)`, `set(options)` (CookieStore.set format), `delete(name|options)`. Cross-subdomain cookies need a custom domain — impossible on `netlify.app` (Public Suffix List).\n- **`next(options?)`** / **`next(request, options?)`** — invoke the next item in the chain; returns a `Promise<Response>` you can modify. `options.sendConditionalRequest: true` for conditional requests. Only call `next` if you need the response body. Pass an explicit `Request` when you've read the body.\n- **`params`** — path params, e.g. path `/pets/:name` + request `/pets/winter` → `{name:\"winter\"}`. Query string: use `request.url`.\n- **`ip`** — client IP string.\n- **`requestId`** — Netlify request ID.\n- **`account.id`**, **`site.{id,name,url}`**, **`server.region`**, **`deploy.{context,id,published,skewProtectionToken}`**.\n- **`waitUntil(promise)`** — extend execution past the response (analytics, logs) without blocking it. Still subject to the CPU limit.\n\n**`Netlify` global:** `Netlify.context` (null outside the handler), `Netlify.env.{get,has,set,delete,toObject}`. `Netlify.env.set`/`delete` are **invocation-scoped only** — they do not persist env vars; use the Netlify env API endpoints.\n\n## Response caching\n\n⚠️ **Caching requires BOTH opting in AND setting headers — it's both or neither.** Setting `Cache-Control` on the returned `Response` does nothing without `cache: \"manual\"` in config, and vice versa. Default (either missing): every request invokes the function.\n\n1. Opt in: `cache: \"manual\"` (inline or `netlify.toml`).\n2. Set headers **inline in the function code** (not in `netlify.toml`):\n\n```ts\nimport type { Context, Config } from \"@netlify/edge-functions\";\n\nexport default async (req: Request, context: Context) => {\n return new Response(\"Hello world\", {\n headers: { \"cache-control\": \"public, s-maxage=3600\" },\n });\n};\n\nexport const config: Config = { cache: \"manual\", path: \"/hello\" };\n```\n\nSupported cache headers: `Cache-Control`, `CDN-Cache-Control`, `Netlify-CDN-Cache-Control`, `Expires` (overridden by `max-age`/`s-maxage`), `Vary`, `Netlify-Vary`. See https://docs.netlify.com/build/caching/caching-overview\n\n**Atomic deploys void the cache:** `s-maxage`/`max-age`/`Expires` are discarded by a new deploy in the same deploy context, even mid-lifetime.\n\n**When to cache:** endpoint responses reusable across clients (e.g. identical SSR HTML). **Do not cache** middleware, routing/transform logic, or per-client personalization.\n\n⚠️ **Caching functions always shadow static files.** A caching function on `/*` serves `/cat.png` instead of the static `cat.png`.\n\n## Error handling (`onError`, inline only)\n\n- **`fail`** (default) — serve a generic error page.\n- **`/YOUR_CUSTOM_PATH`** — rewrite to a same-site path (must start with `/`); served without invoking edge functions for that path.\n- **`bypass`** — skip the erroring function, continue the chain.\n\n```ts\nexport const config: Config = { path: \"/hello\", onError: \"/unavailable\" };\n```\n\nFail closed for critical logic (auth); fail open (`bypass`) for progressive enhancement (nice-to-have localization).\n\n## Environment variables\n\n- Set via UI/CLI/API; scope **must include Functions** to reach edge runtime.\n- **Env vars in `netlify.toml` are NOT available to edge functions.**\n- **Build-scope vars are NOT available at edge runtime** — only during the build step. Embed their values at build time if needed.\n- Changes require a **new build and deploy**; each deploy freezes values at deploy time.\n- Access at runtime with `Netlify.env.get(key)` / `Netlify.env.toObject()`.\n\n```ts\nexport default async (request: Request, context: Context) => {\n const value = Netlify.env.get(\"MY_IMPORTANT_VARIABLE\");\n return new Response(`Value: ${value}`);\n};\n```\n\nNext.js Middleware note: with Netlify Edge Functions for Middleware on Next.js, `process.env` also works.\n\n## Runtime & modules\n\nDeno-based. Import modules by:\n- **Node built-ins:** `import { randomBytes } from \"node:crypto\";`\n- **Deno/URL imports:** `import React from \"https://esm.sh/react\";`\n- **npm packages (beta):** `npm install` then import by name. ⚠️ Beta — packages using native binaries (Prisma) or runtime dynamic imports (cowsay) may fail.\n\n**Import maps** (module names instead of URLs) — use a separate import map file, declared in `netlify.toml`:\n\n```toml\n[functions]\n deno_import_map = \"./path/to/your/import_map.json\"\n```\n\nSupported Web APIs include `fetch`/`Request`/`Response`/`URL`/`File`/`Blob`, `console`, `atob`/`btoa`, `TextEncoder`/`TextDecoder` (+ stream variants), Web Crypto (`randomUUID`, `getRandomValues`, `SubtleCrypto`), WebSocket, timers, Streams API, URLPattern, `Performance`.\n\n## Local dev & deploy\n\n```bash\nnpm install netlify-cli -g\nnetlify dev # runs edge functions on local requests\n# visit http://localhost:8888/test\n```\n\n- Debug: `netlify dev` with `--edge-inspect` or `--edge-inspect-brk` (see https://cli.netlify.com/commands/dev/).\n- Geo mocking: `--geo=mock` (San Francisco) or `--geo=mock --country=XX`.\n- ⚠️ **No local caching** — cache headers are ignored in local testing.\n- Manual deploys require **Netlify CLI 12.2.8+** (older versions error).\n- Deploys are **atomic** — old deploys keep old behavior until you publish a new production deploy.\n\n**Monitor:** production logs at Netlify UI **Cloud compute > Edge functions**. Each `console.*` log includes the generating function name. Retention ≥ 24h (7 days on some plans). Log Drains on Enterprise.\n\n## Limits & feature gaps\n\n- **Code size:** 20 MB compressed (bundle max).\n- **Memory:** 512 MB per set of deployed edge functions.\n- **CPU time:** 50 ms per request (excludes wait time; `waitUntil` work still counts).\n- **Response header timeout:** 40 s.\n- Cached responses do **not** count toward invocations.\n- **Split Testing** enabled → edge functions do **not** run.\n- **Custom Headers** (incl. basic auth) do **not** apply to edge functions.\n- **Prerendering** does not apply to edge-served paths.\n- Rewrites are **same-site only** — use `fetch` for other/external sites.\n- Multiple framework plugins generating edge functions may collide.\n- **Not** supported under HIPAA-compliant hosting.\n\nSee the overview at https://docs.netlify.com/build/edge-functions/overview.md and the full example library at https://edge-functions-examples.netlify.app/\n\n<!-- system: agent-context/edge-functions/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (edge-functions)\n\nThese are org conventions and field-learned guardrails, not docs facts — they\nare merged into the rendered skill by ctx-gen and are never generated.\nExtracted from the previous hand-written netlify-edge-functions skill; owned\nby the skills maintainer.\n\n1. Check the framework's adapter/reference first: a custom edge function that\n duplicates adapter-generated middleware causes conflicts. Only hand-write\n an edge function when the framework doesn't already generate one for the\n job.\n2. Scope `path` narrowly. `path: \"/*\"` intercepts every request — including\n static assets — adding latency to each one and billing an edge invocation\n for it.\n3. An edge function without a route (no config export, no netlify.toml\n declaration) still deploys, but silently never runs: no build error, no\n warning. When \"my edge function does nothing\", check the route first.\n4. Choose edge vs serverless by workload shape: edge functions for low-latency\n request/response manipulation, geolocation logic, auth checks/redirects,\n and A/B personalization; serverless functions for long-running work (up to\n 15 min), heavy Node.js dependencies, database-heavy operations,\n background/scheduled tasks, or memory needs above 512 MB.\n5. Cache headers on an edge response do nothing without `cache: \"manual\"` in\n config — it's both or neither. Setting `Cache-Control` on the returned\n `Response` has no effect unless the function also opts in.\n6. When explaining declaration processing order, state the two-pass loop,\n not just the ordering: Netlify runs the whole declaration order TWICE —\n first pass runs only edge functions not configured for caching, second\n pass runs the ones with caching configured. \"Non-cached before cached\"\n without the loop framing is an incomplete answer.\n"
}SHA-256 of public snapshot: 1abb1f0eae1431f9dddd5332836494d2c5c6beddd0b35f99b4a8921b7b6e766f