🔀 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 aRequestHandler
that adds CORS headers to the request. This allows enabling CORS on one specific route+server.ts
without needing to modifyhooks.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.
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.
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
.
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
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
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
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.