Orbit

Edge Functions

Edge functions are small JavaScript handlers that run on Kapsule's edge network before a request reaches your project, so you can do redirects, header injection, A/B routing and bot filtering without a round trip to your app. This guide covers writing one, the entry-point forms accepted, path matching, how multiple functions interact, deploying, and what happens when a function throws.

Where They Live

Open your project in Orbit and click the Edge functions tab. Every function belongs to a project and is listed with its status badge: LIVE, PAUSED, NOT DEPLOYED or DEPLOY FAILED.

Edge functions tab of an Orbit project

Creating a Function

  1. Click New function.
  2. Fill in Name (up to 120 characters).
  3. Fill in Path pattern, the URL pattern this function should run on.
  4. Choose a Trigger: Request (before origin), Response (after origin), or Both.
  5. Choose an Environment scope: All environments, Production only, or Preview only.
  6. Write the handler in Function body.
  7. Click Save, then Deploy to edge.

The request object is always in scope, and the source is capped at 64 KB.

Writing a Handler

Return a Response to answer the request at the edge. Return nothing, or undefined, to pass the request through to your project unchanged.

export default {
  async fetch(request) {
    const url = new URL(request.url)

    // Redirect /old to /new
    if (url.pathname === '/old') {
      return new Response(null, {
        status: 301,
        headers: { Location: '/new' },
      })
    }

    // Returning nothing passes through to the origin
  },
}

The named-function form works too:

export default async function handler(request) {
  // Add a security header to every response
  const response = await fetch(request)
  const headers = new Headers(response.headers)
  headers.set('X-Frame-Options', 'DENY')
  return new Response(response.body, { status: response.status, headers })
}

Supported Entry-Point Forms

FormExample
Object with a fetch methodexport default { async fetch(request) { ... } }
Object with an arrow fetch propertyexport default { fetch: async (request) => { ... } }
Named function declarationexport default async function handler(request) { ... }
Bare body, no exportWrite the statements directly, with no wrapper

Anything else that uses export default, such as export default class, is rejected when you click Save, with an error naming the two forms to use instead. Validation runs at save time rather than at deploy time, so you find out immediately and a broken function is never published.

Path Patterns

The Path pattern decides which requests run the function. It must start with / and can be up to 2048 characters. * matches any run of characters and ? matches a single character. A pattern with no wildcard matches that exact path and everything beneath it.

PatternMatches
/*Every path
/api/*Anything starting with /api/
/blog/*/commentsFor example /blog/my-post/comments
/page/page, and anything under /page/

The request Object

request is a standard Fetch API Request. You can read the URL, method, headers and body:

export default {
  async fetch(request) {
    const url    = new URL(request.url)
    const cookie = request.headers.get('cookie') ?? ''
    const ua     = request.headers.get('user-agent') ?? ''

    if (ua.includes('BadBot')) {
      return new Response('Forbidden', { status: 403 })
    }
  },
}

Do not assume a header exists because you have seen it on another platform. Read the headers your own function actually receives (log them from the function, or return them in a debug response on a throwaway path) before you branch on one. A handler that branches on a header that is never present silently takes the wrong path on every request.

Common Patterns

Redirect Old URLs

export default {
  async fetch(request) {
    const url = new URL(request.url)
    const redirects = {
      '/old-about':   '/about',
      '/old-contact': '/contact',
    }
    const dest = redirects[url.pathname]
    if (dest) return Response.redirect(url.origin + dest, 301)
  },
}

For a handful of straightforward path-to-path moves, use the built-in redirect engine instead: it needs no code and is configured in Settings. See Configuring Redirects and Rewrites.

Add Security Headers

export default async function handler(request) {
  const response = await fetch(request)
  const headers = new Headers(response.headers)
  headers.set('X-Frame-Options', 'DENY')
  headers.set('X-Content-Type-Options', 'nosniff')
  headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers,
  })
}

For static header rules you do not need code either. Settings has a response headers section with quick-add presets for HSTS, CSP, no-embed, no-sniff, referrer policy and CORS.

A/B Routing

export default {
  async fetch(request) {
    const url     = new URL(request.url)
    const variant = Math.random() < 0.5 ? 'a' : 'b'
    url.searchParams.set('variant', variant)
    return fetch(url.toString(), request)
  },
}

Environment Scope

ScopeRuns on
All environmentsProduction, staging and every branch preview
Production onlyThe production environment
Preview onlyEvery non-production environment

Ship a new function as Preview only first, confirm it behaves on a branch preview, then switch it to All environments. An edge function runs in front of every request, so a mistake in one is a mistake on every page at once.

How Multiple Functions Interact

All of your project's enabled functions are evaluated in the order they were created. For each request, the edge walks the list and runs the first function whose path pattern matches the request path and whose scope covers the environment.

  • If that function returns a Response, it is sent and evaluation stops.
  • If it returns nothing, evaluation continues to the next matching function.
  • If none of them returns a Response, the request goes to your project as normal.

This means a broad /* function created early can shadow a narrower one created later, if the broad one returns a Response. Create the specific ones first, or make the broad one return nothing for paths it should not handle.

Deploying

Click Deploy to edge. The deploy regenerates a single combined router for the whole edge network from the current state of the database, so enabling, disabling, editing or deleting any function republishes everything. Changes usually take effect within a few seconds.

Each function row keeps a Deploy log showing the steps of the last deploy, and a DEPLOY FAILED badge with the error if it did not succeed.

Pause takes a function out of the router without deleting it, which is the quickest way to back out a misbehaving function. Resume puts it back.

When a Function Throws

An exception inside a function is caught at the edge. The error is logged and the request falls through to your project as if the function had returned nothing.

This is a safety net, not a monitoring system. A function that throws on every request fails silently from your visitors' point of view, and your traffic simply behaves as though the function does not exist. If a function stops having its intended effect, suspect an exception before you suspect the routing.

Limits

  • Up to 20 functions per project. The 21st is rejected.
  • Up to 64 KB of source per function.
  • Up to 120 characters for the name, and 2048 characters for the path pattern.

Related Reading

Still need help?

Email us at support@kapsulehost.com or open a chat in KPanel.

Open KPanel