@data-eden/network
Features
- fetch-compatible Create a middleware-enabled
fetch
with the same API as window.fetch
, compatible with anything that understands fetch
. - request & response aware Supports middlewares for observing or altering requests or responses, alone or together.
- streaming-compatible Adds middleware support without eager body consumption, so can be used with streaming, as long as your middlewares are written to be streaming-aware.
- composable Middleware can be composible (e.g. you can author a middleware in terms of other middlewares)
Example use Cases
- CSRF Injection - add a CSRF header before every request
- Query Tuneling - seamlessly encode requests with long URLs as
POST
requests - Analytics - send client-analytics for request,response pairs
- Response Transformation - seamlessly transform responses, e.g. for compatibility
API
export type Middleware = (request: Request, next: (request: Request) => Promise<Response>) : Promise<Response>;
export interface BuildFetchOptions {
disablePrior?: boolean;
disableMessage?: string;
}
export function buildFetch(
middlewares: Middleware[],
options?: BuildFetchOptions
): typeof fetch;
Middleware Examples
type Fetch = typeof fetch;
async function noopMiddleware(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
return next(request);
}
async function csrfMiddleware(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
request.headers.set('X-CSRF', 'a totally legit request');
return next(request);
}
async function queryTunneling(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
if (request.url.length <= MaxURLLength) {
return next(request);
}
let url = new URL(request.url);
request.headers.set('X-HTTP-Method-Override', request.method);
let tunneledRequest = new Request(
`${url.protocol}//${url.hostname}${url.pathname}`,
{
method: 'POST',
headers: request.headers,
body: url.searchParams,
}
);
return next(tunneledRequest);
}
async function analyticsMiddleware(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
let response = await next(request);
let requestHeaders = [...request.headers.keys()];
let responseHeaders = [...response.headers.keys()];
let status = response.status;
let contentType = response.headers.get('content-type');
let analyticsEntries = [];
if (/^application\/json/.test(contentType)) {
let responseJson = await response.clone().json();
if (responseJson.has_interesting_property) {
analyticsEntries.push('interesting');
}
}
scheduleAnalytics({
requestHeaders,
responseHeaders,
status,
analyticsEntries,
});
return response;
}
async function batchCreateEmbedResource(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
if (/target\/url\/pattern/.test(request.url)) {
return next(request);
}
let stashedRequest = request.clone();
let rawResponse = await next(request);
let contentType = rawResponse.headers.get('content-type');
if (!/^application\/json/.test(contentType)) {
return rawResponse;
}
let transformedResponse = rawResponse.clone();
transformedResponse.json = async function () {
let requestBody = await stashedRequest.json();
let responseBody = await rawResponse.json();
for (let i = 0; i < responseBody.elements.length; ++i) {
responseBody.elements[i].resource = requestBody.elements[i];
}
};
return transformedResponse;
}
async function badMiddleware(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
let response = await next(request);
let responseJson = await response.json();
if (responseJson.something) {
}
return response;
}
Middleware Composition
Composing middleware is as easy as composing normal functions.
async function limitedAnalytics(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
if (request.url.startsWith('/api')) {
return await analyticsMiddleware(request, fetch);
}
return next(request);
}
Fetch Usage
import { buildFetch } from '@data-eden/network';
let fetch = buildFetch([
csrfMiddleware,
queryTunneling,
limitedAnalytics,
batchCreateEbmedResource,
]);
await fetch('///api');
let response = await fetch('/my-api');
Middleware Configuration
There are two patterns for configuring middleware:
- Factory functions
- Middleware HTTP headers
If your middleware has setup-time configuration, use a factory function:
export function buildAnalyticsMiddleware(analyticsUrl) {
return new async (
request: Request,
next: (request: Request) => Promise<Response>
) => {
let analytics = extractAnalytics(request);
scheduleAnalytics(analyticsUrl, analytics);
return next(request);
}
};
If your middleware requires per-request user configuration, you can use custom
HTTP headers as a means of communicating from the user's fetch
call to your
middleware. If needed, your middleware can transform these headers to provide
better APIs to your users than what your server may allow.
async function analyticsMiddleware(
request: Request,
next: (request: Request) => Promise<Response>
): Promise<Response> {
let useCase = request.headers.get('X-Use-Case');
if(useCase !== undefined) {
request.headers.delete('X-Use-Case');
request.headers.set('X-Server-Header-Tracking-abc123': useCase);
}
return next(request);
}
For highly configurable middlewares these techniques can be combined.
Prior Art