@equinor/fusion-framework-vite-plugin-api-service
Version:
Vite plugin for proxying service discovery and mocking
180 lines (159 loc) • 5.94 kB
text/typescript
import httpProxy from 'http-proxy';
import type {
ApiRoute,
ServerListener,
PluginLogger,
ProxyListener,
IncomingRequest,
} from './types.js';
import { createRouteMatcher } from './create-route-matcher.js';
import { InvalidRouteError, validateRoute } from './validate-route.js';
import { createResponseInterceptor } from './create-response-interceptor.js';
/**
* Options for processing a route in the Vite plugin API services.
*
* @property {PluginLogger} [logger] - An optional logger instance for logging purposes.
* @property {ProxyListener} [onProxyRes] - An optional listener that is triggered on proxy responses.
*/
export type ProcessRouteOptions = {
logger?: PluginLogger;
onProxyRes?: ProxyListener;
};
/**
* Processes an API route by validating it, handling middleware, and optionally
* setting up a proxy server for the route.
*
* @param route - The API route configuration object.
* @param args - The parameters passed to the server listener, typically including
* the request (`req`), response (`res`), and next middleware function (`next`).
* @param options - Optional configuration for the processing, including:
* - `logger`: A logger instance for logging debug, warning, and error messages.
* - `onProxyRes`: A callback for handling proxy response events.
*
* @throws {InvalidRouteError} If the request URL is missing or the route validation fails.
*
* @remarks
* - Validates the route to ensure it has the required structure.
* - If the route has a `middleware` function defined, it will be executed instead of setting up a proxy.
* - If both `middleware` and `proxy` are defined, the `middleware` takes precedence.
* - If the route has a `proxy` configuration, a proxy server is created and configured.
* - The `proxy.configure` method, if provided, is called to allow additional customization of the proxy server.
* - Logs debug, warning, and error messages using the provided logger, if available.
*/
function processesRoute(
route: ApiRoute,
args: Parameters<ServerListener>,
options?: ProcessRouteOptions,
): void {
const { logger } = options ?? {};
const [req, res, next] = args;
try {
validateRoute(route);
} catch (error) {
logger?.error((error as Error).message);
next(error);
return;
}
const requestUrl = req.url;
if (!requestUrl) {
next(new InvalidRouteError('missing request url'));
return;
}
// if route has middleware, execute it
if (route.middleware) {
logger?.info(`executing route middleware on match ${route.match} -> ${req.originalUrl}`);
if (route.proxy) {
logger?.warn('route.middleware and route.proxy are both defined. Using middleware');
}
route.middleware(req, res, next);
return;
}
const { configure, rewrite, transformResponse, ...proxyOptions } = route.proxy;
if (rewrite) {
req.url = rewrite(req.url ?? '');
}
const proxyServer = httpProxy.createProxyServer({
selfHandleResponse: !!transformResponse,
prependPath: true,
secure: process.env.NODE_ENV === 'production',
changeOrigin: true,
...proxyOptions,
});
proxyServer.on('error', (err) => {
logger?.error(`proxy for ${requestUrl} to ${proxyOptions.target} failed: ${err.message}`);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end(`Proxy error: ${err.message}`);
});
proxyServer.on('proxyReq', (proxyReq, req: IncomingRequest) => {
// Set the original request URL
logger?.info(
`Proxying ${req.originalUrl} -> ${proxyReq.protocol}//${proxyReq.host}${proxyReq.path}`,
);
});
proxyServer.on('proxyRes', (proxyRes, req: IncomingRequest) => {
const { headers, statusMessage, statusCode = 500 } = proxyRes;
const message = `Received response for ${req.originalUrl} -> ${statusCode} ${statusMessage}`;
res.writeHead(statusCode, {
...headers,
'x-proxy-rewrite-target': proxyOptions.target,
});
if (statusCode ?? 0 >= 400) {
logger?.error(message);
logger?.debug({
request: {
url: req.originalUrl,
headers: req.headers,
},
response: {
statusCode,
statusMessage,
headers,
},
});
} else {
logger?.debug(message);
}
});
if (options?.onProxyRes) {
logger?.debug('adding custom onProxyRes handler');
proxyServer.on('proxyRes', options.onProxyRes);
}
if (transformResponse) {
logger?.debug('adding response interceptor');
proxyServer.on('proxyRes', createResponseInterceptor(transformResponse, { logger }));
}
// if provided route has configure method, call it
if (configure) {
logger?.debug('configuring proxy server');
configure(proxyServer, proxyOptions);
}
proxyServer.web(req, res);
}
/**
* Processes a list of API routes by matching the incoming request URL against each route.
* If a match is found, the corresponding route is processed and the middleware chain is terminated.
* If no match is found, the `next` middleware function is called to continue the chain.
*
* @param routes - An array of API routes to be processed.
* @param middlewareArgs - The middleware arguments, which include the request object, response object, and the `next` function.
* @param options - Optional configuration for processing the route.
*/
export function processRoutes(
routes: ApiRoute[],
middlewareArgs: Parameters<ServerListener>,
options?: ProcessRouteOptions,
): void {
const [req, _res, next] = middlewareArgs;
for (const route of routes) {
if (!req.url) continue;
const match = createRouteMatcher(route)(req.url ?? '', req);
if (match) {
const [req, res, next] = middlewareArgs;
req.params = typeof match === 'object' ? match.params : {};
processesRoute(route, [req, res, next], options);
return;
}
}
// no route matched, continue middleware chain
next();
}