将 axios 的请求记录打印成 cURL
正如 JSON 已经成为普遍的对象序列化格式一样,cURL 也应该是 HTTP 请求的普遍序列化格式。JSON 原本是为 JavaScript 使用的,其本名是 JavaScript Object Notation,但是各种语言都喜欢用它了,连 Java 这个强类型语言也总是要和它打交道,哪怕 JSON 里缺少类型信息,以至于在 Java 中会碰到各种不便。
题外话,这种不便纯粹是强类型导致的,并不是说序列化和反序列化有多难。比如,对于 JavaScript 程序员来说,很熟悉 JSON.stringify 和 JSON.parse。对于 Java 来说,只要放松类型要求(统统 Object),很容易实现一个:
package helpers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class JsonHelper {
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
public static String stringify(Object anything) {
return gson.toJson(anything);
}
public static Object parse(String s) {
return gson.fromJson(s, Object.class);
}
}
package helpers;
import org.junit.jupiter.api.Test;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.*;
class JsonHelperTest {
void testStringify() {
Object o = new Object();
String res = JsonHelper.stringify(o);
assertEquals("{}", res);
}
...
总之,JSON 已经成为了编程语言中的对象的非常受欢迎的序列化形式。
那么,对于 HTTP 请求,像 JSON 一样受欢迎的序列化形式,就应该是 cURL。
关于 cURL,已经在一篇文章中推荐过:《使用 cURL 提高前后端开发连调中的沟通效率》,这里再补充一点。cURL 一旦拷贝出来,可以方便导入到 Postman,并转化成各种其他语言代码导出:
序列化成 cURL,就是方便重放。一般都可以从抓包工具或者开发者工具来以 cURL 复制前端发出的请求,但其实,很多“后端”也是要再次调用另外的后端服务的,这些请求,比较难以抓包(当然可以使用一些像 w2 这样的工具,但是有很强的代码侵入性,并且需要在部署上下点工夫)。对于后端发出的请求,比较自然的方式就是打印日志,但是分散打印,再手动拼装完全没有必要,应该实现在代码里,自动生成 cURL。
对于 Java 服务,如果使用 FeignClient 来发请求,之前写过一篇《将 FeignClient 的请求记录成 cURL 格式》。而对于 node 服务,常见的是使用 axios 来发送 HTTP 请求,同样,也非常推荐将 axios 的请求记录成 cURL 格式。
附上代码:
TypeScript 版:
import { curlirize, Method } from './curlirize'
describe('curlirize', () => {
it('curlize axios requests', () => {
const config = {
method: Method.GET,
params: { q: 's' },
headers: {},
url: 'url',
baseURL: 'base/',
}
const res = curlirize(config)
expect(res).toEqual('cURL to replay: curl -X GET "base/url?q=s" ')
})
})
import type { AxiosRequestConfig } from 'axios'
import { pickBy, isPlainObject } from 'lodash'
export const curlirize = (config: AxiosRequestConfig) => {
const { headers: rawHeaders, method = 'GET', baseURL = '', url, params: rawParams = {}, data } = config
const headers = {
...rawHeaders?.common,
...(isPlainObject(data)
? { 'Content-Type': 'application/json' }
: rawHeaders
? rawHeaders[method.toLowerCase()]
: {}),
...pickBy(rawHeaders, (_, key) => !Method[key] && key !== 'common'),
}
const query = Array.from(Object.entries(rawParams)).reduce((query, [key, value]) => {
if (value === undefined) return query
if (typeof value === 'object') {
if (Array.isArray(value)) {
value.forEach((item) => query.append(key, item))
} else {
// undefined behavior
}
} else {
query.append(key, `${value}`)
}
return query
}, new URLSearchParams())
const serializedHeaders = headers ? Object.keys(headers).map((key) => `--header "${key}: ${headers[key]}"`) : []
const cmd = `cURL to replay: curl -X ${method.toUpperCase()} "${baseURL}${url}${
query ? `?${query.toString()}` : ''
}" ${serializedHeaders.join(' ')} `
if (isPlainObject(data)) {
return cmd + `--data '${JSON.stringify(data)}'`
} else {
return cmd
}
}
export enum Method {
'get' = 'GET',
'GET' = 'GET',
'delete' = 'DELETE',
'DELETE' = 'DELETE',
'head' = 'HEAD',
'HEAD' = 'HEAD',
'options' = 'OPTIONS',
'OPTIONS' = 'OPTIONS',
'post' = 'POST',
'POST' = 'POST',
'put' = 'PUT',
'PUT' = 'PUT',
'patch' = 'PATCH',
'PATCH' = 'PATCH',
'purge' = 'PURGE',
'PURGE' = 'PURGE',
'link' = 'LINK',
'LINK' = 'LINK',
'unlink' = 'UNLINK',
'UNLINK' = 'UNLINK',
}
纯 JavaScript 版,可以参考这个(比较仓促,写得略简):https://github.com/Jeff-Tian/serverless-space/blob/00064fa2dbd75a6daf48cb206491a35a3eae2ba1/src/common/curlirize.ts