DocsToolsSvelteKit - Handles

🔀 How to - @kitql/handles

đź’ˇ

KitQL itself is not a library, it’s “nothing” but a collection of standalone libraries.

A set of handles and utilities for request handling customization in SvelteKit 🫵!

Installation

npm i -D @kitql/handles

Utilities

  • handleProxies - implements a reverse proxy that forwards from a path to a given external service. Useful for hiding a backing service from clients.
  • handleCors - applies CORS headers to set of defined routes. The configuration options allowed per route are a subset of those used by the cors package, for easy migration.
  • handleCsrf - allows cross-site form submissions to a set of defined routes. The allowed origins for submissions can be configured per route. For all other routes, implements the default CSRF behavior of SvelteKit, where all cross-site form submissions are blocked.
  • createCorsWrapper - creates a wrapper around a RequestHandler that adds CORS headers to the request. This allows enabling CORS on one specific route +server.ts without needing to modify hooks.server.ts.

Required SvelteKit / Vite Configuration Changes

handleCsrf

To use handleCsrf, disable SvelteKit’s default CSRF blocking behavior. handleCsrf will duplicate the behavior of the default CSRF blocking for any routes that are not specifically configured.

svelte.config.js
const config = {
  kit: {
    // ... other kit options
    // disable sveltekit built-in CSRF blocking - this is replaced by `handleCsrf`
    csrf: {
      checkOrigin: false
    }
  }
}

handleCors and createCorsWrapper

Not strictly necessary, but recommended: when using handleCors or createCorsWrapper, disable Vite’s default addition of wildcard CORS headers in development and preview mode. This allows you to test your CORS configuration as it would behave in production.

Note that you need to specify cors: { origin: false } rather than disabling cors entirely with cors: false, as the SvelteKit vite plugin adds its own CORS configuration with cors: { preflightContinue: true }. This is not included in the warning logging for overriden config entries due to a SvelteKit vite plugin issue.

vite.config.ts
export default defineConfig({
  // ... plugins, etc
  // disable automatic CORS header addition in dev
  server: {
    cors: {
      origin: false
    }
  },
  // disable automatic CORS header addition in preview
  preview: {
    cors: {
      origin: false
    }
  }
})

Usage

handleProxies

Creates a handler which, for requests matching a given path prefix in the given options, proxies the request to the to URL in the corresponding ProxyOptions. Any path elements after the matching prefix are preserved, e.g. a request to /from/some/other/path will be proxied to to/some/other/path. The request method, body, and headers are preserved, with the exception of the Host header which is set to the proxy target hostname.

If a requestHook is defined, it is called with the original request event before the returned request is proxied. This allows for modifying the request before it is sent, e.g. to add or remove authentication headers, api keys, etc. If the returned request is to a URL with the same origin as the initial request and a path that also starts with the current path prefix, that request path is proxied; otherwise, the returned request path is not modified before it is fetched. If the requestHook function throws, the request is not proxied and a response corresponding to the thrown object (matching Sveltekit endpoint behavior) is returned to the client instead.

If multiple options entries would match a request, the first matching entry is used.

For requests matching a path prefix in options that do not have an Origin header or that have an Origin header not matching the request’s origin, a 403 Forbidden response is returned. This prevents use of the proxy by other browser applications, but (IMPORTANT) does not prevent abuse of the proxy by applications that can set the Origin header manually to match. To prevent this, you can require user authentication and authorization before allowing proxying, validating either in a preceding hook or directly in the requestHook.

src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks'
 
import { handleProxies } from '@kitql/handles'
 
export const handle = sequence(
  // Forwards all requests to paths beginning with `/proxy` to
  // `http://my.super.website/graphql`. Subpaths are preserved, e.g. `/proxy/api/path` is
  // forwarded to `http://my.super.website/graphql/api/path`.
  handleProxies([['/proxy', { to: 'http://my.super.website/graphql' }]])
)

This way, customers will never see the url http://my.super.website/graphql.

handleCors

src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks'
 
import { handleCors } from '@kitql/handles'
 
export const handle = sequence(
  handleCors([
    // default options: set origin to '*', allow methods 'GET,HEAD,PUT,PATCH,POST,DELETE',
    // reflect request `Access-Control-Request-Headers` value to `Access-Control-Allow-Headers`
    ['/api/cors-handler/default-options', {}],
    // reflect request origin to `Access-Control-Allow-Origin`, use other defaults
    [/some-path-regex\/*\/reflect/, { origin: true }],
    [
      '/api/cors-handler/complex-options',
      {
        // allow only trusted origins, defined by either string or matching regex
        origin: ['http://google.com', /trusted-domain/],
        // allow only certain methods
        methods: ['GET', 'PUT'],
        // customize allowed headers
        allowedHeaders: 'X-Allowed-Header',
        // customize exposed headers
        exposedHeaders: 'X-Exposed-Header',
        // set `Access-Control-Allow-Credentials` header to 'true'
        credentials: true,
        // set `Access-Control-Max-Age` header to 42
        maxAge: 42
        // return 200 status code rather than 204 for preflight requests
        optionsStatusSuccess: 200,
      }
    ]
  ])
)

createCorsWrapper

src/routes/api/endpoint/+server.ts
import { json } from '@sveltejs/kit'
 
import { createCorsWrapper } from '@kitql/handles'
 
const wrapper = createCorsWrapper({
  origin: ['http://my-trusted-origin.dev', /other-trusted-origin-regex/],
  methods: ['GET', 'POST']
  // other options, matching those for `handleCors`
})
 
// default options handler, which returns the status code defined in the
// `createCorsWrapper` options, or 204 if none is provided.
export const OPTIONS = wrapper.OPTIONS
 
// alternatively, you can define your own options handler if you need to
// customize the response, e.g. with additional headers.
export const OPTIONS = wrapper(
  () => new Response(null, { status: 204, headers: { 'X-Custom-Header': 'custom value' } }),
)
 
// wrap your request handlers with the wrapper to correctly set CORS headers
export const GET = wrapper(() => json({ message: 'Success message' }))
export const POST = wrapper((event) => { ... })

handleCsrf

Creates a handler which blocks cross-site form submissions not explicitly enabled by the given options. The logic is ported from the native SvelteKit CSRF prevention logic, with the addition of the ability to selectively disable this protection for specific routes and (optionally) limit the allowed request origins for cross-site form submissions for those routes. See: respond.js

If a form submission request’s origin does not match the target URL origin, the request is checked against the provided options. If the request’s path matches a path in the options, and the request origin is allowed by the origin in the options, the request is allowed to proceed.

Any requests not matching a path in the options, or for which the request origin is not allowed, are blocked with status 403.

The logic for detecting which requests should be subject to CSRF protection is also ported from SvelteKit. A request is subject to CSRF protection if:

  • the request origin does not match the target URL origin (i.e. the app origin)
  • the method is POST, PUT, PATCH, or DELETE
  • the content type is application/x-www-form-urlencoded, multipart/form-data, or text/plain
src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks'
 
import { handleCsrf } from '@kitql/handles'
 
export const handle = sequence(
  handleCsrf([
    // allow cross-site form submissions from all origins
    ['/api/csrf-handler/all-origins', { origin: true }],
    // allow cross-site form submissions from only certain origins
    [(/\/api\/csrf-handler\/some-origins/, { origin: ['http://google.com', /trusted-domain/] })]
  ])
)

Configuration Options

handleProxies: ProxyOptions

to

to: string

The URL to which to proxy requests.

requestHook

optional requestHook: (event: RequestEvent) => MaybePromise<Request>

A function that is called with the original request event before the request is proxied. The function can modify the request before it is sent, e.g. to add or remove authentication headers, api keys, etc. If the returned request is to a URL with the same origin as the initial request and a path that also starts with the matched path prefix, that request path is proxied; otherwise, the returned request path is not modified before it is fetched. If the requestHook function throws, the request is not proxied and a response corresponding to the thrown object (matching Sveltekit endpoint behavior) is returned to the client instead.

handleProxies: OptionsByStringPath

An array of tuples, where the first element is a string to match against the request URL pathname, and the second element is the options to apply in the event of a match. A request matches if the request URL pathname begins with the provided string. If multiple entries match a request, the first matching entry is used.

handleCors and handleCsrf: OptionsByPath

An array of tuples, where the first element is a string or RegExp to match against the request URL pathname, and the second element is the options to apply in the event of a match. If a string is provided, the request URL pathname must begin with the provided string; if a RegExp is provided, it is tested against the request URL pathname using RegExp.test. If multiple entries match a request, the first matching entry is used.

handleCors and createCorsWrapper: CorsOptions

origin?

optional origin: boolean | string | RegExp | (string | RegExp)[]

If true, reflects request origin in Access-Control-Allow-Origin. If set to a specific string, sets Access-Control-Allow-Origin to that value. If a RegExp or an array of strings/RegExps, reflects the request origin in Access-Control-Allow-Origin if it matches any of the strings / RegExps provided. If explicitly set to false or undefined, does not set the Access-Control-Allow-Origin header. Defaults to '*'.

allowedHeaders?

optional allowedHeaders: string | boolean | string[]

Sets Access-Control-Allow-Headers to the given string or array of strings (joined with ,). If set to true, reflects the Access-Control-Request-Headers header. If explicitly set to false, or undefined, does not set the Access-Control-Allow-Headers header. Defaults to true.

credentials?

optional credentials: boolean

Sets Access-Control-Allow-Credentials to true if true, or unset if false. Defaults to false.

exposedHeaders?

optional exposedHeaders: string | string[]

Sets Access-Control-Expose-Headers to the given string or array of strings (joined with ,). If not specified, does not set Access-Control-Expose-Headers.

maxAge?

optional maxAge: number

Sets Access-Control-Max-Age to the given number. If unset, does not set Access-Control-Max-Age.

methods?

optional methods: string | string[]

Sets Access-Control-Allow-Methods to the given string or array of strings (joined with ,). If explicitly set to undefined, does not set the Access-Control-Allow-Methods header. Defaults to 'GET,HEAD,PUT,PATCH,POST,DELETE'.

optionsStatusSuccess?

optional optionsStatusSuccess: number

If set, returns the given status code for preflight requests. If unset, returns 204. Useful for clients that fail if an OPTIONS request returns 204 (mostly legacy browsers).

handleCsrf: CsrfOptions

origin

origin: boolean | string | RegExp | (string | RegExp)[]

If true, allows all origins to perform cross-site form submissions. If set to a specific string, a RegExp, or an array of strings/RegExps, allows cross-site form submissions if the request origin matches the provided allowed origins. If set to false, cross-site form submissions are blocked.