Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access final GraphQL-Reponse in nest.js with interceptor

I have implemented a LoggingInterceptor which should be able to access the final GraphQL-Response with its data- and error properties + the original request body and the authenticated user, which has been added to the request by AuthGuard before.(EDIT: Partially solved by @jay-mcdoniel: user and body are accessible through GqlExecutionContext.create(context).getContext())

Indeed the Interceptor just provides one fully resolved GraphQL-Object.

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(tap(
      (allData) => console.log(allData),
      (error)=> console.log(error)));
  }
}

This is my Interceptor-Class. It's just calling the RxJS-Operator tap to log the current values of the observable.

If I run the following GraphQL-Request...

mutation {
  login(data: { username: "admin", password: "123456" }) {
    id
    username
    token
  }
}

... my server answers correctly with the following response-body:

{
  "data": {
    "login": {
      "id": "6f40be3b-cda9-4e6d-97ce-ced3787e9974",
      "username": "admin",
      "token": "someToken"
    }
  }
}

But the content of allData which get's logged to console by my interceptor is the following:

{
  id: '6f40be3b-cda9-4e6d-97ce-ced3787e9974',
  isAdmin: true,
  username: 'admin',
  firstname: null,
  lastname: null,
  email: null,
  created: 2019-07-05T15:11:31.606Z,
  token: 'someToken'
}

Instead I would like to see the information of the real response-body.

I have additionally tried to access the HttpResponse by context.switchToHttp().getResponse(). But this only contains the parameters of the mutation-login-method:

{
  data: [Object: null prototype] { username: 'admin', password: '123456' }
}

EDIT:

console.log(GqlExecutionContext.create(context).getContext()); prints (still no GraphQL-ResponseBody):

{
  headers: {
    /*...*/
  },
  user: /*...*/,
  body: {
    operationName: null,
    variables: {},
    query: 'mutation {\n  login(data: {username: "admin", password: ' +
      '"123456"}) {\n    token\n    id\n    username\n    isAdmin\n  }\n' +
      '}\n'
  },
  res: ServerResponse {
    _events: [Object: null prototype] { finish: [Function: bound resOnFinish] },
    _eventsCount: 1,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: true,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: false,
    _headerSent: false,
    socket: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      readable: true,
      _events: [Object],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: true,
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      timeout: 120000,
      parser: [HTTPParser],
      on: [Function: socketOnWrap],
      _paused: false,
      _httpMessage: [Circular],
      [Symbol(asyncId)]: 566,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        /*...*/
      },
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    connection: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _readableState: [ReadableState],
      readable: true,
      _events: [Object],
      _eventsCount: 8,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: true,
      allowHalfOpen: true,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: [Server],
      _server: [Server],
      timeout: 120000,
      parser: [HTTPParser],
      on: [Function: socketOnWrap],
      _paused: false,
      _httpMessage: [Circular],
      [Symbol(asyncId)]: 566,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: 120000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 3273,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(asyncId)]: 567,
        [Symbol(triggerId)]: 566
      },
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    _header: null,
    _onPendingData: [Function: bound updateOutgoingData],
    _sent100: false,
    _expect_continue: false,
    req: IncomingMessage {
      /*...*/
    },
    locals: [Object: null prototype] {},
    [Symbol(isCorked)]: false,
    [Symbol(outHeadersKey)]: [Object: null prototype] {
      'x-powered-by': [Array],
      'access-control-allow-origin': [Array]
    }
  },
  _extensionStack: GraphQLExtensionStack { extensions: [ [CacheControlExtension] ] }
}
like image 695
rweisse Avatar asked Jul 08 '19 09:07

rweisse


2 Answers

The interceptor is actually called before and after the response, or it should be at least, so that you can have pre-request logic (request in) and post-request logic (response out). You should be able to do all pre-request processing before you call next.hanlde() and then you should be able to use the RxJS Observable operators such as tap or map after a pipe() call. Your allData variable should have all the information from the request/response, and you can even use the context variable for getting even more information.

What does allData currently print for you? Have you tried GqlExecutionContext.create(context).getContext().req or GqlExecutionContext.create(context).getContext().res? These are shown being used in the Guards documentation to get the request and response objects like you would with a normal HTTP call.

like image 52
Jay McDoniel Avatar answered Oct 18 '22 10:10

Jay McDoniel


That's how I did that in my Logging Interceptor

    @Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private readonly logger: Logger) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // default REST Api
    if (context.getType() === 'http') {
     ...
     ...
    }

    // Graphql
    if (context.getType<GqlContextType>() === 'graphql') {
      const gqlContext = GqlExecutionContext.create(context);
      const info = gqlContext.getInfo();
      const res: Response = gqlContext.getContext().res;
      // Get user that sent request
      const userId = context.getArgByIndex(2).req.user.userId;
      const parentType = info.parentType.name;
      const fieldName = info.fieldName;
      const body = info.fieldNodes[0]?.loc?.source?.body;
      const message = `GraphQL - ${parentType} - ${fieldName}`;

      // Add request ID,so it can be tracked with response
      const requestId = uuidv4();
      // Put to header, so can attach it to response as well
      res.set('requestId', requestId);

      const trace = {
        userId,
        body
      };

      this.logger.info(`requestId: ${requestId}`, {
        context: message,
        trace
      });
      
      return next.handle().pipe(
        tap({
          next: (val: unknown): void => {
            this.logNext(val, context);
          }
        })
      );
    }
    return next.handle();
  }

  /**
   * Method to log response message
   */
  private logNext(body: unknown, context: ExecutionContext): void {
    // default REST Api
    if (context.getType() === 'http') {
      ...
      ...
    }

    if (context.getType<GqlContextType>() === 'graphql') {
      const gqlContext = GqlExecutionContext.create(context);
      const info = gqlContext.getInfo();
      const parentType = info.parentType.name;
      const fieldName = info.fieldName;
      const res: Response = gqlContext.getContext().res;
      const message = `GraphQL - ${parentType} - ${fieldName}`;

      // Remove secure fields from request body and headers
      const secureBody = secureReqBody(body);

      const requestId = res.getHeader('requestId');

      // Log trace message
      const trace = {
        body: { ...secureBody }
      };
      this.logger.info(`requestId: ${requestId}`, {
        context: message,
        trace
      });
    }
  }
}
like image 21
Konstantin Z Avatar answered Oct 18 '22 11:10

Konstantin Z