通过 Nginx 日志恢复失败请求的若干问题

发布于 2022/11/5, 编辑于 2022/11/8

工作上遇到了一个需求, 有一个接口因为 body 量相当大, 很容易请求失败, 加上是个静默接口用户不会有任何感知, 不会手动重新发起请求. 于是就需要我们从 nginx 日志上根据收集的参数信息进行接口请求恢复, 本文即记录做该需求中遇到的若干问题和解决方案

相关

问题综述

由于需要恢复请求, 所以使用脚本, 考虑到后续维护, 用 node 脚本进行开发, 脚本功能如下:

  • 从日志文件 (.csv) 中解析出相关的参数
  • 根据参数恢复请求
  • 输出结果日志

开发中遇到的问题:

  • 工作环境需要开启代理才能请求得到目标 url, 明明环境已经具有 http_proxyhttps_proxy 但 node 脚本却无法发出这个请求
  • 日志的 body 被转义并转码了, 恢复这些编码遇到了问题

node 中的代理

根据 https://github.com/nodejs/node/issues/1490 得知 node 原生不实现代理功能, 也不读取 http_proxy 变量, 原因是加入 proxy 的话 node 开发团队就需要考虑安全 / SOCKS / 证书等等一系列问题, 于是不做

但是依然可以通过很多库实现这个功能, 例如: node-global-proxy

import proxy from "node-global-proxy";

export function setupProxy(address: string) {
  proxy.setConfig({ http: address, https: address });

  proxy.start();
}

setupProxy(proxyAddress); // 程序初始化时设置代理

// ...其他代码

编码问题

下面的字符串 "x\xC2\x9C\xC3", 只是一个代指, 不是一个有意义的值, 只是为了方便解释

这个问题就比较复杂了, 主要体现在两个地方:

  1. 转码失败. 运维把 nginx 日志的编码格式设置成了 utf-16, 根据 https://juejin.cn/post/7122019278991458334#heading-2 可知, js 的编码也是 utf-16, 是可以直接使用从日志中得到的字符串, 然而却出现了两种情况
    1. 直接给 js 变量赋值例如: const body = "x\xC2\x9C\xC3" 再使用这个 body 是能成功发出请求的, js 自动完成了转码, 其参数和客户端的请求参数完全一样
    2. 从 .csv 日志中通过 readline 解析出字符串 "x\xC2\x9C\xC3" 再赋值给 body, 却没有自动转码, 传的参数是 "x\xC2\x9C\xC3" 这个字符串而不是转码后的字符串
  2. 转义失败. node 中的 url 转义恢复: 熟悉前端开发可知请求参数经常需要先通过 encodeURIComponent 转义再发请求, 原因这里不提, 问题在于这次使用 decodeURIComponent 后居然无法正确恢复解析转义

转义失败问题

先说第二个问题, 直接使用 decodeURIComponent("x\xC2\x9C\xC3") 无效 (报错了), 那么也就意味着 decodeURIComponent 对这些初始 utf-16 编码的字符串依然无法正确解析

然后通过搜索找到了另一个函数: querystring.escape, 有效, 但是不是完全有效, 我发现这个函数只对部分的 utf-16 有用, 有的就无效, 这就有点奇怪了, 再仔细找原因, 发现了 node:querystring 文档里就有提到: https://nodejs.org/api/querystring.html#querystringunescapestr

By default, the querystring.unescape() method will attempt to use the JavaScript built-in decodeURIComponent() method to decode. If that fails, a safer equivalent that does not throw on malformed URLs will be used.
大概翻译下:
默认情况下 querystring.unescape() 方法会尝试用 decodeURIComponent() 进行转义, 如果失败了, 那么使用更安全的方法来解决那些长得不像 url 的字符串

也就是 pass 掉

最终找到了这个: https://stackoverflow.com/questions/37670485/how-do-i-decode-this-string-xc3-x99-xc3-xa9-xc2-x87-bx-xc2

使用 decodeURIComponent(escape(s)) 即可, 原来是还需要用 escape 先做一层转换呀, 不过值得一提的是 escape 函数已经被标记 deprecated

转码失败问题

上面提到过, js 是 utf-16 编码的, 理论上直接支持来自 nginx 的日志参数, 直接赋值也证明了这一点 (下面 nextFetch 会直接使用 body 发出 fetch 请求)

const body = "x\xC2\x9C\xC3";
console.log(body); // 被转码的字符串

nextFetch(body); // 成功

然而如果是用 node:readline 读文件中的 'x\xC2\x9C\xC3' 却无法直接使用:

// nginx-log.csv
x\xC2\x9C\xC3
let body = "";

const fileStream = createReadStream("./nginx-log.csv");
const rl = createInterface({
  input: fileStream,
  crlfDelay: Infinity,
});

rl.on("line", function (line) {
  body = line;
});

rl.on("close", function () {
  console.log(body); // 没有被转码的字符串 "x\xC2\x9C\xC3"
  nextFetch(body); // 失败
});

造成这个结果的原因其实是, js 代码编译过程中其实已经把第一种情况的字符串转码了, 到 runtime 时使用的已经是转码后的字符串

而第二种情况是此时 js 程序已经是 runtime 了, 此时得到的 line 自然不是代码的一部分, 就没有经过编译这个过程而未被转码, 只得到了原始的字符串

那么我们直接转码即可, 参考: https://gist.github.com/kiinlam/176ce20707336fa8278726e869e59cb1

export function decodeUtf16(s: string) {
  return s.replace(/\\x(\w{2})/g, function (_, $1) {
    return String.fromCharCode(parseInt($1, 16));
  });
}

点击这里前往 Github 查看原文,交流意见~

文档信息

版权声明:自由转载 - 非商用 - 非衍生 - 保持署名(创意共享3.0许可证