使用 NodeJS 拦截 HTTP 请求
简介
作为工作项目的一部分,我需要开发一种方法来拦截和存储任何给定后端应用程序(在本例中为微服务)的 HTTP 流量。这本来是一个相当简单的任务,但我们的后端由许多服务(和许多代码库)组成。因此,该解决方案必须尽可能无缝衔接,以便能够轻松集成到任何服务中。
TLDR;
使用@mswjs/interceptors
可以直接拦截后端应用程序上的 HTTP 流量。
拦截HTTP流量
对于我的用例,我能想到的捕获 HTTP 流量的方法有两种:
- 创建一个包装 HTTP 客户端库(如 Axios)的库
- 以某种方式拦截所有 HTTP 流量
理想情况下,我会选择方案 1,因为它最简单。可惜的是,我负责的项目包含许多由不同团队负责的微服务。因此,每个人回头重构代码以使用这个新库都会很困难。
因此,我唯一的选择实际上就是选项 2。
第一次尝试
我的第一次尝试还算成功,但远非完美。在尝试直接通过底层http模块拦截流量后,我选择了一种更高级的解决方案。我的想法是对Axios 的请求方法进行monkey patch,以便在请求发送之前和响应接收之后注入我自己的逻辑。
function _instrumentAxios(axiosInstance: AxiosInstance) {
axiosInstance.request = _instrumentHttpRequest(axiosInstance.request, axiosInstance);
axiosInstance.get = _instrumentHttpRequest(axiosInstance.get, axiosInstance, "get");
axiosInstance.post = _instrumentHttpRequest(axiosInstance.post, axiosInstance, "post");
axiosInstance.put = _instrumentHttpRequest(axiosInstance.put, axiosInstance, "put");
axiosInstance.patch = _instrumentHttpRequest(axiosInstance.patch, axiosInstance, "patch");
axiosInstance.delete = _instrumentHttpRequest(axiosInstance.delete, axiosInstance, "delete");
axiosInstance.options = _instrumentHttpRequest(axiosInstance.options, axiosInstance, "options");
}
function _instrumentHttpRequest(originalFunction: Function, thisArgument: any, method?: string) {
return async function() {
const {method: cleanedMethod, url, config: requestConfig, data} = _parseAxiosArguments(arguments, method);
const requestEvent: HttpRequestEvent = {
url,
method: cleanedMethod,
body: data,
headers: requestConfig?.headers,
};
// Intentionally not waiting for a response to avoid adding any latency with this instrumentation
doSomethingWithRequest(requestEvent);
const res = await originalFunction.apply(thisArgument, arguments);
const responseEvent: HttpResponseEvent = {
url,
method: cleanedMethod,
body: res.data,
headers: res.headers,
statusCode: res.status,
};
doSomethingWithResponse(responseEvent);
return res;
};
}
这种方法效果很好,但是我在阅读 Axios 文档时偶然发现了一种更简洁的方法。
第二次尝试
令我惊讶的是,Axios 实际上提供了一个用于拦截请求和响应的 API!
import {createInterceptor, InterceptorApi, IsomorphicRequest, IsomorphicResponse} from "@mswjs/interceptors";
import {interceptXMLHttpRequest} from "@mswjs/interceptors/lib/interceptors/XMLHttpRequest";
import {interceptClientRequest} from "@mswjs/interceptors/lib/interceptors/ClientRequest";
function _instrumentAxios(axiosInstance: AxiosInstance) {
axiosInstance.interceptors.request.use(_instrumentHttpRequest);
axiosInstance.interceptors.response.use(_instrumentHttpResponse);
}
function _instrumentHttpRequest(requestConfig: AxiosRequestConfig): AxiosRequestConfig {
const method = String(requestConfig.method);
const headers = requestConfig.headers && {
...requestConfig.headers.common,
...requestConfig.headers[method],
};
const requestEvent: HttpRequestEvent = {
headers,
method,
url: String(requestConfig.url),
body: requestConfig.data,
};
// Intentionally not waiting for a response to avoid adding any latency with this instrumentation
doSomethingWithRequest(requestEvent);
return requestConfig;
}
function _instrumentHttpResponse(response: AxiosResponse): AxiosResponse {
const responseEvent: HttpResponseEvent = {
url: String(response.request?.url),
method: String(response.request?.method),
body: response.data,
headers: response.headers,
statusCode: response.status,
};
// Intentionally not waiting for a response to avoid adding any latency with this instrumentation
doSomethingWithResponse(responseEvent);
return response;
}
啊!好多了。然而,这种方法还有另一个麻烦,第一次尝试时也遇到了:必须为每个 Axios 实例设置拦截;这会降低开发者的体验。我最初以为大家都用的是默认的 Axios 实例。然而,事实证明,也可以通过 创建新实例axios.create()
。所以,回到绘图板 😔
最终解决方案
在尝试处理底层http
模块之前,我决定先寻找一些现有的解决方案。经过一段时间的摸索,我偶然发现了这个库@mswjs/interceptors
。这个库文档非常完善,并且对 TypeScript 友好。
function _instrumentHTTPTraffic() {
const interceptor = createInterceptor({
resolver: () => {}, // Required even if not used
modules: [interceptXMLHttpRequest, interceptClientRequest],
});
interceptor.on("request", _handleHttpRequest);
interceptor.on("response", _handleHttpResponse);
interceptor.apply();
}
function _handleHttpRequest(request: IsomorphicRequest): void {
const url = request.url.toString();
const method = String(request.method);
const headers = request.headers.raw();
const requestEvent: HttpRequestEvent = {
headers,
method,
url: request.url.toString(),
body: request.body,
};
// Intentionally not waiting for a response to avoid adding any latency with this instrumentation
doSomethingWithRequest(requestEvent);
}
function _handleHttpResponse(request: IsomorphicRequest, response: IsomorphicResponse): void {
const url = request.url.toString();
const headers = request.headers.raw();
const responseEvent: HttpResponseEvent = {
url: request.url.toString(),
method: request.method,
body: response.body,
headers: response.headers.raw(),
statusCode: response.status,
};
// Intentionally not waiting for a response to avoid adding any latency with this instrumentation
doSomethingWithResponse(responseEvent);
}
卡维亚茨
虽然最终的解决方案更加通用,并且与所使用的客户端 HTTP 库无关,但仍存在一些缺点:
- 由于所有流经应用的 HTTP 流量都会被拦截,因此需要一些逻辑来判断哪些请求需要忽略。例如,像 NewRelic 这样的监测工具会定期发送请求来捕获元数据。如果处理不当,这可能会增加很多干扰。
- 依赖其他库。这是否严重取决于拦截的用途。对于大多数项目来说,可能不是什么大问题。