@worker-tools/middleware
Version:
A suite of standalone HTTP server middlewares for Worker Runtimes.
196 lines (160 loc) • 6.88 kB
text/typescript
// deno-lint-ignore-file
import { notAcceptable, unsupportedMediaType } from "@worker-tools/response-creators";
import negotiated from 'negotiated';
import { Awaitable } from './utils/common-types.js';
import { Context } from './index.js'
const weightSortFn = <X extends { weight: number }>(a: X, b: X) => a.weight >= b.weight ? a : b;
const ACCEPT = 'Accept';
const ACCEPT_ENCODING = 'Accept-Encoding';
const ACCEPT_LANGUAGE = 'Accept-Language';
const CONTENT_TYPE = 'Content-Type';
const CONTENT_LANGUAGE = 'Content-Language';
const CONTENT_ENCODING = 'Content-Encoding';
const VARY = 'Vary';
export interface ContentType<T> {
/** The best content type _acceptable to the client_. */
type: T,
}
export interface ContentLanguage<T> {
/** The best language _acceptable to the client_. */
language: T,
}
export interface ContentEncoding<T> {
/** The best encoding _acceptable to the client_. */
encoding: T,
}
export interface Accepted<T> {
/** The request's `Content-Type` header iff acceptable to this endpoint */
accepted: T,
}
export interface AcceptedLanguage<T> {
/** The request's `Language` header if (and only if) accepted by this endpoint */
acceptedLanguage: T,
}
export interface AcceptedEncoding<T> {
/** The request's `Encoding` header if (and only if) accepted by this endpoint */
acceptedEncoding: T,
}
/**
* Performs content negotiation over the content type of the response.
* @param types The content types _provided_ by this endpoint.
*/
export function contentTypes<T extends string, TS extends readonly T[]>(
types: TS
): <X extends Context>(ax: Awaitable<X>) => Promise<X & ContentType<TS[number]>> {
return async ax => {
const ctx = await ax;
const { headers } = ctx.request;
const type = [...negotiated.mediaTypes(headers.get(ACCEPT)) as any]
.filter(t => !types || types.includes(t.type))
.reduce(weightSortFn, { weight: -1 }).type as TS[number]
if (headers.has(ACCEPT) && types && !type) throw notAcceptable();
ctx.effects.push(response => {
if (!response.headers.has(CONTENT_TYPE)) response.headers.set(CONTENT_TYPE, type)
// If the server accepts more than 1 option, we set the vary header for correct caching
if ((types?.length ?? 0) > 1) response.headers.append(VARY, ACCEPT);
return response;
})
return Object.assign(ctx, { type })
}
}
/**
* Performs content negotiation over the content language of the response.
* @param languages The languages _provided_ by this endpoint.
*/
export function contentLanguages<T extends string, TS extends readonly T[]>(
languages: TS
): <X extends Context>(ax: Awaitable<X>) => Promise<X & ContentLanguage<TS[number]>> {
return async ax => {
const ctx = await ax;
const { headers } = ctx.request;
const language = [...negotiated.languages(headers.get(ACCEPT_LANGUAGE)) as any]
.filter(l => !languages || languages.includes(l.language))
.reduce(weightSortFn, { weight: -1 }).language as TS[number]
if (headers.has(ACCEPT_LANGUAGE) && languages && !language) throw notAcceptable();
ctx.effects.push(response => {
if (!response.headers.has(CONTENT_LANGUAGE)) response.headers.set(CONTENT_LANGUAGE, language)
// If the server accepts more than 1 option, we set the vary header for correct caching
if ((languages?.length ?? 0) > 1) response.headers.append(VARY, ACCEPT_LANGUAGE);
return response;
})
return Object.assign(ctx, { language })
}
}
/**
* Performs content negotiation over the content encoding of the response.
* @param encodings The encodings _provided_ by this endpoint.
*/
export function contentEncodings<T extends string, TS extends readonly T[]>(
encodings: TS
): <X extends Context>(ax: Awaitable<X>) => Promise<X & ContentEncoding<TS[number]>> {
return async ax => {
const ctx = await ax;
const { headers } = ctx.request;
const encoding = [...negotiated.encodings(headers.get(ACCEPT_ENCODING)) as any]
.filter(e => !encodings || encodings.includes(e.encoding))
.reduce(weightSortFn, { weight: -1 }).encoding as TS[number];
// TODO: how to handle status errors in middleware??
if (headers.has(ACCEPT_ENCODING) && encodings && !encoding) throw notAcceptable();
ctx.effects!.push(response => {
if (!response.headers.has(CONTENT_ENCODING)) response.headers.set(CONTENT_ENCODING, encoding)
// If the server accepts more than 1 option, we set the vary header for correct caching
if ((encodings?.length ?? 0) > 1) response.headers.append(VARY, ACCEPT_ENCODING);
return response
})
return Object.assign(ctx, { encoding })
}
}
export {
contentTypes as provides,
contentLanguages as providesLanguages,
contentEncodings as providesEncodings,
}
/**
* Determines if a request body content type is _acceptable_ to this endpoint.
* @param types The content types _acceptable_ to this endpoint.
*/
export function accepts<T extends string, TS extends readonly T[]>(
types: TS
): <X extends Context>(ax: Awaitable<X>) => Promise<X & Accepted<TS[number]>> {
return async ax => {
const ctx = await ax;
const { headers } = ctx.request;
const accepted =
[...negotiated.mediaTypes(headers.get(CONTENT_TYPE))][0]?.type as TS[number];
if (types?.length && !types.includes(accepted)) throw unsupportedMediaType();
return Object.assign(ctx, { accepted })
}
}
/**
* Determines if a request body content language is _acceptable_ to this endpoint.
* @param languages The languages (of the request body) _acceptable_ to this endpoint.
*/
export function acceptsLanguages<T extends string, TS extends readonly T[]>(
languages: TS
): <X extends Context>(ax: Awaitable<X>) => Promise<X & AcceptedLanguage<TS[number]>> {
return async ax => {
const ctx = await ax;
const { headers } = ctx.request;
const acceptedLanguage =
[...negotiated.languages(headers.get(CONTENT_LANGUAGE)) as any][0]?.language as TS[number];
if (languages?.length && !languages.includes(acceptedLanguage)) throw notAcceptable();
return Object.assign(ctx, { acceptedLanguage })
}
}
/**
* Determines if a request body content encoding is _acceptable_ to this endpoint.
* @param encodings The body encodings _acceptable_ to this endpoint.
*/
export function acceptsEncodings<T extends string, TS extends readonly T[]>(
encodings: TS
): <X extends Context>(ax: Awaitable<X>) => Promise<X & AcceptedEncoding<TS[number]>> {
return async ax => {
const ctx = await ax;
const { headers } = ctx.request;
const acceptedEncoding =
[...negotiated.encodings(headers.get(CONTENT_ENCODING)) as any][0]?.encoding as TS[number];
if (encodings?.length && !encodings.includes(acceptedEncoding)) throw notAcceptable();
return Object.assign(ctx, { acceptedEncoding })
}
}