簡體   English   中英

nestjs 中間件獲取請求/響應正文

[英]nestjs middleware get request/response body

我正在使用 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

ServerResponse { domain: null, _events: { finish: [Function: bound resOnFinish] }, _eventsCount: 1, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last:false,升級:false,chunkedEncoding:false,shouldKeepAlive:true,useChunkedEncodingByDefault:true,sendDate:true,_removedConnection:false,_removedContLen:true,_removedTE:true,_contentLength:0,_hasBody:false,_trailer:'',完成: true, _headerSent: true, socket: null, connection: null, _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:假, req: IncomingMessage { _readableState: ReadableState { objectMode: false, highWaterMark: 16384, 緩沖區: [Object], 長度: 0, 管道: null, pipeCount: 0, 流動: true, 結束: true, endEmitted: false ,閱讀:假,同步:真,needReadable:假,發射可讀:真,可讀監聽:假,resumeScheduled:真,銷毀:假,defaultEncoding:'utf8',awaitDrain:0,readingMore:真,解碼器:空,編碼:空},可讀:真,域:空,_events:{},_eventsCount:0,_maxListeners:未定義,套接字:套接字{連接:假,_hadError:假,_handle:[對象],_parent:空,_host:空,_readableState : [Object], 可讀: true, 域: null, _events: [Object], _eventsCount: 10, _maxListeners: undefined, _writableState: [Object], writable: true, allowHalfOpen: true, _bytesDispatched: 155, _sockname: null, _pendingData : null, _pendingEncoding: '', server: [Object], _server: [Object], _idleTimeout: 5000, _idleNext: [Object], _idlePrev: [Object], _idleStart: 12562, _destroyed: false, parser: [Object], on: [Function: socketOnWrap], _paused: false, read: [Function], _sumption: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asy) ncId)]: 153, [Symbol(triggerAsyncId)]: 151 }, connection: Socket {connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], 可讀: true,域:null,_events:[Object],_eventsCount:10,_maxListeners:未定義,_writableState:[Object],可寫:true,allowHalfOpen:true,_bytesDispatched:155,_sockname:null,_pendingData:null,_pendingEncoding:' , server: [Object], _server: [Object], _idleTimeout: 5000, _idleNext: [Object], _idlePrev: [Object], _idleStart: 12562, _destroyed: false, parser: [Object], on: [Function: socketOnWrap] , _paused: false, read: [Function], _sumption: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asyncId)]: 153, [Symbol(triggerAsyncId) )]: 151 }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { host: 'localhost:5500', 'user-agent': 'Mozilla/5.0 (X11; 烏班圖; Linux x86_64; RV:64.0)的Gecko / 20100101火狐/ 64.0' ,接受: 'text / html的,是application / xhtml + xml的,應用/ XML; Q = 0.9,/ Q = 0.8', '接受語言':“EN-US ,en;q=0.5', 'accept-encoding': 'gzip, deflate', 連接: 'keep-alive', 'upgrade-insecure-requests': '1', 'if-none-match': 'W /"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' }, rawHeaders: [ 'Host', 'localhost:5500', 'User-Agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/ 20100101火狐/ 64.0' , '接受', 'text / html的,是application / xhtml + xml的,應用/ XML; q = 0.9,/ q = 0.8', '接受語言',“EN-US,EN; q =0.5', 'Accept-Encoding', 'gzip, deflate', 'Connection', 'keep-alive', 'Upgrade-Insecure-Requests', '1', 'If-None-Match', 'W/' 19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"'], 預告片: {}, rawTrailers: [], upgrade: false, url: '/hello-world', method: 'GET', statusCode: null, statusMessage: null, client:套接字{連接:false,_hadError:false,_handle:[Object],_parent:null,_host:null,_readableState:[Object],可讀:true,域:null,_events:[對象] ct], _eventsCount: 10, _maxListeners: 未定義, _writableState: [Object], writable: true, allowHalfOpen: true, _bytesDispatched: 155, _sockname: null, _pendingData: null, _pendingEncoding: '', server: [Object], _server: [Object], _idleTimeout: 5000, _idleNext: [Object], _idlePrev: [Object], _idleStart: 12562, _destroyed: false, parser: [Object], on: [Function: socketOnWrap], _paused: false, read: [Function ], _sumption: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asyncId)]: 153, [Symbol(triggerAsyncId)]: 151 }, _sumption: false , _dumped: true, next: [Function: next], baseUrl: '', originalUrl: '/hello-world', _parsedUrl: Url { protocol: null, slashes: null, auth: null, host: null, port: null ,主機名:空,哈希:空,搜索:空,查詢:空,路徑名:'/hello-world',路徑:'/hello-world',href:'/hello-world',_raw:'/hello- world' }, params: {}, query: {}, res: [Circular], body: {}, route: Route { path: '/hello-world', stack: [Array], methods : [Object] } }, locals: {}, statusCode: 304, statusMessage: 'Not Modified', [Symbol(outHeadersKey)]:​​ { 'x-powered-by': [ 'X-Powered-By', 'Express '], etag: [ 'ETag', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"'] } }

在響應對象中沒有出現 {"message":"Hello World"} 消息,如果可能的話,我想知道如何從 res 和 req 對象中獲取正文。

注意:我知道 nestjs 有Interceptors ,但是按照文檔的說明,中間件應該是解決這個問題的方法。

我不小心遇到了這個問題,它列在與我的問題“相關”中。

關於響應,我可以進一步擴展Kim Kern 的回答

響應的問題是響應主體不是響應對象的屬性,而是 為了能夠獲得它,您需要覆蓋寫入該流的方法。

就像 Kim Kern 已經說過的那樣,你可以看看這個線程,有一個公認的答案如何做到這一點。

或者您可以使用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 可以為您提供:

  • 攔截器,就像你說的。 文檔中有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);
}

但請注意,這將在攔截之前執行,因為此裝飾器更改方法行為。

而且我不認為這是進行日志記錄的好方法,只是出於多樣性提到了它:)

令人難以置信的是,如此瑣碎的事情如此難以做到。

記錄響應正文的更簡單方法是創建一個攔截器https://docs.nestjs.com/interceptors ):

應用模塊

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

Http攔截器

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;
      }),
    );
  }
}

響應正文將無法作為屬性訪問。 請參閱此線程以獲取解決方案。

然而,你應該能夠訪問請求主體req.body因為巢用途bodyParser默認。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM