【问题标题】:nestjs middleware get request/response bodynestjs 中间件获取请求/响应体
【发布时间】:2019-06-03 17:50:30
【问题描述】:

我正在为一个项目使用nestjs,并希望记录尽可能多的信息,其中之一就是每个http 请求的响应和请求的主体。为此我制作了一个嵌套中间件:

import {token} from 'gen-uid';
import { inspect } from 'util';
import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';
import { Stream } from 'stream';
import { createWriteStream, existsSync, mkdirSync } from 'fs';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    logfileStream: Stream;

    constructor() {
        if (!existsSync('./logs')) mkdirSync('./logs');
        this.logfileStream = createWriteStream("./logs/serviceName-"+ new Date().toISOString() + ".log", {flags:'a'});
    }

resolve(...args: any[]): MiddlewareFunction {
    return (req, res, next) => {
        let reqToken = token();
        let startTime = new Date();
        let logreq = {
            "@timestamp": startTime.toISOString(),
            "@Id": reqToken,
            query: req.query,
            params: req.params,
            url: req.url,
            fullUrl: req.originalUrl,
            method: req.method,
            headers: req.headers,
            _parsedUrl: req._parsedUrl,
        }

        console.log(
            "timestamp: " + logreq["@timestamp"] + "\t" + 
            "request id: " + logreq["@Id"] + "\t" + 
            "method:  " + req.method + "\t" +
            "URL: " + req.originalUrl);

        this.logfileStream.write(JSON.stringify(logreq));

        const cleanup = () => {
            res.removeListener('finish', logFn)
            res.removeListener('close', abortFn)
            res.removeListener('error', errorFn)
        }

        const logFn = () => {
            let endTime = new Date();
            cleanup()
            let logres = {
                "@timestamp": endTime.toISOString(),
                "@Id": reqToken,
                "queryTime": endTime.valueOf() - startTime.valueOf(),
            }
            console.log(inspect(res));
        }

        const abortFn = () => {
            cleanup()
            console.warn('Request aborted by the client')
        }

        const errorFn = err => {
            cleanup()
            console.error(`Request pipeline error: ${err}`)
        }

        res.on('finish', logFn) // successful pipeline (regardless of its response)
        res.on('close', abortFn) // aborted pipeline
        res.on('error', errorFn) // pipeline internal error

        next();
    };
}
}

然后我将此中间件设置为全局中间件来记录所有请求,但是查看 res 和 req 对象,它们都没有属性。

在代码示例中,我设置了要打印的响应对象,在我的项目上运行一个 hello world 端点,该端点返回 {"message":"Hello World"} 我得到以下输出:

时间戳:2019-01-09T00:37:00.912Z 请求 ID:2852f925f987 方法:GET URL:/hello-world

服务器响应 { 域:空, _events:{完成:[功能:绑定resOnFinish]}, _eventsCount:1, _maxListeners:未定义, 输出: [], 输出编码:[], 输出回调:[], 输出大小:0, 可写:真, _last: 假的, 升级:假, 分块编码:假, 应该保持活动:真, useChunkedEncodingByDefault: true, 发送日期:真, _removedConnection:假, _removedContLen:真, _removedTE:真, _contentLength: 0, _hasBody:假, _预告片: '', 完成:真的, _headerSent:真, 套接字:空, 连接:空, _header: 'HTTP/1.1 304 Not Modified\r\nX-Powered-By: Express\r\nETag: W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"\r\n日期: 2019 年 1 月 9 日,星期三 00:37:00 GMT\r\n连接:保持活动状态\r\n\r\n', _onPendingData:[功能:绑定updateOutgoingData], _sent100:假, _expect_continue:假, 要求: 传入消息 { _可读状态: 可读状态 { 对象模式:假, 高水位:16384, 缓冲区:[对象], 长度:0, 管道:空, 管道数:0, 流动的:真实的, 结束:真的, endEmitted:假, 阅读:错误, 同步:真, 需要可读:假, 发射可读:真, 可读听力:假, 恢复计划:真, 毁坏:假, 默认编码:'utf8', 等待排水:0, 阅读更多:真的, 解码器:空, 编码:空}, 可读:真实, 域:空, _事件:{}, _eventsCount:0, _maxListeners:未定义, 插座: 插座 { 连接:假, _hadError:假, _handle:[对象], _父:空, _主机:空, _可读状态:[对象], 可读:真实, 域:空, _events:[对象], _eventsCount:10, _maxListeners:未定义, _writableState:[对象], 可写:真, allowHalfOpen:真, _bytesDispatched: 155, _sockname:空, _pendingData:空, _pendingEncoding: '', 服务器:[对象], _server:[对象], _idleTimeout: 5000, _idleNext: [对象], _idlePrev: [对象], _idleStart:12562, _destroyed:假, 解析器:[对象], 上:[功能:socketOnWrap], _暂停:假, 阅读:[功能], _消费:真的, _httpMessage:空, [符号(asyncId)]:151, [符号(字节读取)]:0, [符号(asyncId)]:153, [符号(triggerAsyncId)]: 151 }, 联系: 插座 { 连接:假, _hadError:假, _handle:[对象], _父:空, _主机:空, _可读状态:[对象], 可读:真实, 域:空, _events:[对象], _eventsCount:10, _maxListeners:未定义, _writableState:[对象], 可写:真, allowHalfOpen:真, _bytesDispatched: 155, _sockname:空, _pendingData:空, _pendingEncoding: '', 服务器:[对象], _server:[对象], _idleTimeout: 5000, _idleNext: [对象], _idlePrev: [对象], _idleStart:12562, _destroyed:假, 解析器:[对象], 上:[功能:socketOnWrap], _暂停:假, 阅读:[功能], _消费:真的, _httpMessage:空, [符号(asyncId)]:151, [符号(字节读取)]:0, [符号(asyncId)]:153, [符号(triggerAsyncId)]: 151 }, httpVersionMajor: 1, httpVersionMinor:1, http版本:'1.1', 完整:真实, 标题: {主机:'本地主机:5500', '用户代理': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0', 接受:'text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8', '接受语言': 'en-US,en;q=0.5', '接受编码':'gzip,放气', 连接:'保持活动', '升级不安全请求':'1', 'if-none-match': 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' }, 原始标题: [ '主持人', '本地主机:5500', '用户代理', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0', '接受', 'text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8', '接受语言', 'en-US,en;q=0.5', '接受编码', 'gzip,放气', '联系', '活着', '升级-不安全-请求', '1', '如果没有匹配', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"'], 预告片:{}, rawTrailers: [], 升级:假, 网址:'/你好世界', 方法:'GET', 状态码:空, 状态消息:空, 客户: 插座 { 连接:假, _hadError:假, _handle:[对象], _父:空, _主机:空, _可读状态:[对象], 可读:真实, 域:空, _events:[对象], _eventsCount:10, _maxListeners:未定义, _writableState:[对象], 可写:真, allowHalfOpen:真, _bytesDispatched: 155, _sockname:空, _pendingData:空, _pendingEncoding: '', 服务器:[对象], _server:[对象], _idleTimeout: 5000, _idleNext: [对象], _idlePrev: [对象], _idleStart:12562, _destroyed:假, 解析器:[对象], 上:[功能:socketOnWrap], _暂停:假, 阅读:[功能], _消费:真的, _httpMessage:空, [符号(asyncId)]:151, [符号(字节读取)]:0, [符号(asyncId)]:153, [符号(triggerAsyncId)]: 151 }, _消费:假, _转储:真的, 下一个:[功能:下一个], baseUrl: '', originalUrl: '/hello-world', _parsedUrl: 网址{ 协议:空, 斜线:空, 身份验证:空, 主机:空, 端口:空, 主机名:空, 哈希:空, 搜索:空, 查询:空, 路径名:'/hello-world', 路径:'/你好世界', href: '/你好世界', _raw: '/hello-world' }, 参数:{}, 询问: {}, 资源:[循环], 身体: {}, 路线:路线{路径:'/hello-world',堆栈:[数组],方法:[对象]}}, 当地人:{}, 状态码:304, statusMessage: '未修改', [符号(outHeadersKey)]: { 'x-powered-by': [ 'X-Powered-By', 'Express' ], etag: [ 'ETag', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' ] } }

在响应对象中没有出现 {"message":"Hello World"} 消息,如果可能的话,我想知道如何从 res 和 req 对象中获取正文。

注意:我知道nestjs 有Interceptors,但是按照文档的说法,中间件应该是解决这个问题的方法。

【问题讨论】:

  • 还有问题吗? :-)

标签: javascript node.js json typescript nestjs


【解决方案1】:

我无意中遇到了这个问题,它被列在“相关”到my question。

关于回复,我可以进一步扩展Kim Kern's answer。

响应的问题是响应体不是响应对象的属性,而是流。为了能够获取它,您需要重写写入该流的方法。

就像 Kim Kern 已经说过的,你可以看看this thread,有公认的答案如何做到这一点。

或者你可以使用express-mung中间件,它会为你做这件事,例如:

var mung = require('express-mung');
app.use(mung.json(
  function transform(body, req, res) {
    console.log(body); // or whatever logger you use
    return body;
  }
));

NestJS 还可以为您提供另外两种不同的方式:

  • Interceptors,就像你说的。文档中有LoggingInterceptor 的示例。
  • 您可以为控制器的方法编写装饰器,这将拦截它们的响应。
import { isObservable, from, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

/**
 * Logging decorator for controller's methods
 */
export const LogReponse = (): MethodDecorator =>
  (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) => {

    // save original method
    const original = descriptor.value;

    // replace original method
    descriptor.value = function() { // must be ordinary function, not arrow function, to have `this` and `arguments`

      // get original result from original method
      const ret = original.apply(this, arguments);

      // if it is null or undefined -> just pass it further
      if (ret == null) {
        return ret;
      }

      // transform result to Observable
      const ret$ = convert(ret);

      // do what you need with response data
      return ret$.pipe(
        map(data => {
          console.log(data); // or whatever logger you use
          return data;
        })
      );
    };

    // return modified method descriptor
    return descriptor;
  };

function convert(value: any) {
  // is this already Observable? -> just get it
  if (isObservable(value)) {
    return value;
  }

  // is this array? -> convert from array
  if (Array.isArray(value)) {
    return from(value);
  }

  // is this Promise-like? -> convert from promise, also convert promise result
  if (typeof value.then === 'function') {
    return from(value).pipe(mergeMap(convert));
  }

  // other? -> create stream from given value
  return of(value);
}

请注意,这将在拦截器之前执行,因为这个装饰器会改变方法的行为。

而且我不认为这是进行日志记录的好方法,只是为了多样性而提到它:)

【讨论】:

    【解决方案2】:

    response 正文将无法作为属性访问。请参阅此thread 以获得解决方案。

    但是,您应该能够使用 req.body 访问 request 正文,因为默认情况下 Nest 使用 bodyParser。

    【讨论】:

      【解决方案3】:

      令人难以置信的是,如此微不足道的事情竟然如此难以做到。

      记录响应正文的更简单方法是创建一个拦截器 (https://docs.nestjs.com/interceptors):

      AppModule:

      providers: [
          {
            provide: APP_INTERCEPTOR,
            useClass: HttpInterceptor,
          }
      ]
      

      HttpInterceptor:

      import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
      import { Observable } from 'rxjs';
      import { map } from 'rxjs/operators';
      
      @Injectable()
      export class HttpInterceptor implements NestInterceptor {
        private readonly logger = new Logger(HttpInterceptor.name);
      
        intercept(
          context: ExecutionContext,
          next: CallHandler<any>,
        ): Observable<any> | Promise<Observable<any>> {
          return next.handle().pipe(
            map(data => {
              this.logger.debug(data);
              return data;
            }),
          );
        }
      }
      

      【讨论】:

      • 通过使用它,我只能看到成功的响应,作为数据结构(我正在使用 GraphQL)。例如,如果 auth 保护阻止了查询(message: "Unauthorized" 是响应),则不会调用拦截器。在所有应用内处理发生后,是否有一种已知的方法可以访问原始响应正文?我想记录可能出错的实际细节。
      猜你喜欢
      • 2019-10-26
      • 2022-08-09
      • 2013-08-09
      • 2018-12-21
      • 1970-01-01
      • 1970-01-01
      • 2012-11-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多