NestJS Interceptors: Unable to set HTTP Headers on outgoing requests

Viewed 12564

I am writing APIs in NestJS which have a set of common headers. I decided to use interceptors in order to append headers to outgoing requests. The headers do not get appended to the request and hence the request keeps on failing.

Interceptor

import * as utils from '../utils/utils';
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor
} from '@nestjs/common';
import { HEADERS } from '../middlewares/headers.constant';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { DATA_PARTITION_ID } from '../app.constants';

@Injectable()
export class HeadersInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<void> {
    const ctx = context.switchToHttp();
    const request: Request = ctx.getRequest();

    this.setHeaders(request);

    return next.handle();
  }

  private setHeaders(request): void {
    this.updateHeaders(request, HEADERS.ACCEPT, 'application/json');
    this.updateHeaders(request, HEADERS.CONTENT_TYPE, 'application/json');
    this.updateHeaders(request, HEADERS.ACCEPT_ENCODING, 'gzip, deflate, br');
    this.updateHeaders(
      request,
      HEADERS.DATA_PARTITION_ID,
      DATA_PARTITION_ID
    );
    this.updateHeaders(
      request,
      HEADERS.AUTHORIZATION,
      `Bearer ${utils.parseCookies(request).stoken}`
    );
    this.updateHeaders(request, HEADERS.APP_KEY, '');
  }

  private updateHeaders(
    request: Request,
    property: string,
    value: string
  ): void {
    if (!request.headers.hasOwnProperty(property)) {
      request.headers[property] = value;
    } else {
      void 0;
    }
  }
}

This interceptor simply does one thing: Access the request and append the headers and pass the control to next handler.

Enums

export enum HEADERS {
  DATA_PARTITION_ID = 'Data-Partition-Id',
  AUTHORIZATION = 'Authorization',
  CONTENT_TYPE = 'Content-Type',
  APP_KEY = 'appkey',
  ACCEPT = 'accept',
  ACCEPT_ENCODING = 'accept-encoding'
}

Controller

import { Body, Controller, Post, Req, UseInterceptors } from '@nestjs/common';
import { HeadersInterceptor } from '../interceptors/headers.interceptor';
import { SearchData } from './models/search-data.model';
import { SearchResults } from './models/search-results.model';
import { SearchService } from './search.service';

@Controller('')
@UseInterceptors(new HeadersInterceptor())
export class SearchController {
  constructor(private searchService: SearchService) {}

  @Post('api/search')
  async searchDataById(@Body() searchData: SearchData, @Req() req): Promise<SearchResults> {
    console.log(req.headers);
    return await this.searchService.getSearchResultsById(searchData);
  }
}

Service

import { HttpService, HttpStatus, Injectable } from '@nestjs/common';
import { AppConfigService } from '../app-config/app-config.service';
import { DataMappingPayload } from './models/data-mapping-payload.model';
import { SearchData } from './models/search-data.model';
import { SearchModelMapper } from './search.service.modelmapper';
import { SearchResults } from './models/search-results.model';
import { ServiceException } from '../exception/service.exception';

@Injectable()
export class SearchService {
  constructor(
    private searchModelMapper: SearchModelMapper,
    private configService: AppConfigService,
    private readonly httpService: HttpService
  ) {}

  async getSearchResultsById(searchData: SearchData): Promise<SearchResults> {
    if (searchData.filters.collectionId) {
      console.log(this.configService.appConfig.urls.SEARCH_RESULTS_BY_COLLECTION_ID_URL.replace(
          '${collectionId}',
          searchData.filters.collectionId
        )
      );
      const searchResultsAPI = await this.httpService
        .get(
          this.configService.appConfig.urls.SEARCH_RESULTS_BY_COLLECTION_ID_URL.replace(
            '${collectionId}',
            searchData.filters.collectionId
          )
        )
        .toPromise();
      const kinds = this.searchModelMapper.getUniqueKinds(
        searchResultsAPI.data.results
      );
      const mappingPayload = await this.getDataMapping(kinds);
      return this.searchModelMapper.generateSearchResults(
        kinds,
        mappingPayload,
        searchResultsAPI.data.results
      );
    } else {
      this.raiseException();
    }
  }

  async getDataMapping(kinds: string[]): Promise<[]> {
    const entityKindNames: DataMappingPayload = {
      entityKindNames: kinds
    };
    const dataMappingAPI = await this.httpService
      .post(
        this.configService.appConfig.urls.DATA_CATALOG_SERVICE_URL,
        JSON.stringify(entityKindNames)
      )
      .toPromise();

    return dataMappingAPI.data.entityViewData;
  }

  // To be moved to util functions
  private raiseException(): void {
    throw new ServiceException(
      {
        message: 'This does not have a collection id',
        missing: 'Collection Id',
        code: HttpStatus.BAD_REQUEST
      },
      HttpStatus.BAD_REQUEST
    );
  }
}

When I access req.headers in the controller, I do get all the headers that I needed to set via interceptors.

{
[0]   'accept-encoding': 'gzip, deflate, br',
[0]   'accept-language': 'en-US,en;q=0.9',
[0]   cookie: '',
[0]   'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
[0]   'content-type': 'application/json',
[0]   accept: 'application/json',
[0]   appkey: '',
[0]   'cache-control': 'no-cache',
[0]   'postman-token': 'cb397012-71aa-460a-b66b-28600538faf9',
[0]   host: 'localhost:8080',
[0]   'content-length': '77',
[0]   connection: 'keep-alive',
[0]   'Data-Partition-Id': 'tenant1',
[0]   Authorization: 'Bearer TOKEN_HERE'
[0] }

When I check the logs of the actual request, it says Authorization is null. Which means the request is not intercepted and not being appended with headers.

Has anyone faced similar issue?

5 Answers

If I'm understanding you properly, you want to have the headers added to your outgoing HTTP call from the HttpService. The interceptor in NestJS works on IncomingMessage (incoming requests in general) and ServerResponse (or outgoing responses in general). It does not see things that are sent from the HttpService or any other HTTP client. Instead, you'd need to set the headers at the method level, or at the module level if they are all common values. The HttpModule has a register and registerAsync method that can be used to pass values to every HttpService call, so if you have common headers you can manage them in that:

@Module({
  imports: [
    HttpModule.register({
      headers: {} // object of headers you want to set
    }),
  ]
})
export class MyModule {}

And now when you use httpService.get(url) you'll send the headers with it.

I just want to share my experience here how to add custom header base on an ongoing request

import { REQUEST } from '@nestjs/core';
import { Module, HttpModule } from '@nestjs/common';

@Module({
    imports: [HttpModule.registerAsync({
        useFactory: request => {
            let automated = 0;
            if (request.get('host').includes('localhost')) {
                automated = 1;
            }
            return { headers: { automated } };
        },
        inject: [REQUEST],
    })],
})

I've used the custom provider and injecting the request so I could identify what headers I'll be setting base on the request given, this would be helpful since we could dynamically set any headers base on request given without manually setting it per axios request.

and use it base on NestJS Documentation

@Injectable()
export class CatsService {
    constructor(private httpService: HttpService) {}

    findAll(): Observable<AxiosResponse<Cat[]>> {
        return this.httpService.get('http://localhost:3000/cats');
    }
}

If the headers are always needed for external HTTP calls, you could add an Axios Interceptor directly in the nestJs HttpService the same way he does in this post to log his request.

The important part is :

  1. Make your app.module.ts implement OnModuleInit
  2. Add the method onModuleInit()
  3. In onModuleInit(), add this.httpService.axiosRef.interceptors.request.use(functionThatWillAddHeadersToRequest(config));

config contains all the information needed for the request including the headers.

Now all your outgoing requests that use HttpService should have your HTTP headers.

Axios interceptors github

I use the nest-keycloak-connect dependency to manage the JWT validation for my endpoints. I'm developing endpoints with a microservices fashion. The nestjs backend needs to call another API secured by the same Keycloak. So I need to pass over the same token from the incoming http request. TLDR.

The solution is simple:

  1. Add a guard by implementing the CanActivate interface (I tried with an interceptor but without success)

  2. extract the JWT from the header and put it in the axios default header

    axios.defaults.headers.common['Authorization'] = 'Bearer' + ${token[1];

  3. add that guard in the app module providers.

app.module.ts:

    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
    // {
    //   provide: APP_INTERCEPTOR,
    //   useClass: TokenInterceptor,
    // },

jwt.auth.guard.ts:

import { ExecutionContext, Injectable } from '@nestjs/common';
import axios from 'axios';
import { CanActivate } from '@nestjs/common';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const ctx = context.switchToHttp();
    const request = ctx.getRequest<Request>();
    const authorization = request.headers['authorization'];

    if (authorization !== '') {
      const token = authorization.split(' ');
      //console.info('JwtAuthGuard token', token[1]);
      axios.defaults.headers.common['Authorization'] = `Bearer ${token[1]}`;
    }
    
    return true;
  }
}

client.ts:

import { HttpService, Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { AxiosResponse } from 'axios';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import coreConfig from 'src/environment/core.config';
import axios from 'axios';

@Injectable()
export class Client {
  constructor(
    private readonly http: HttpService,
  ) {}

  findAll(): Observable<Array<string>> {
    console.info('axios.defaults.headers', axios.defaults.headers);
    const url = 'http://localhost:3000/api2/test';
    console.info('calling url', url);
    return this.http.get(url).pipe(
      map((axiosResponse: AxiosResponse) => {
        return axiosResponse.data;
      }),
    );
  }
}

test.controller.ts:

import { Controller, Get } from '@nestjs/common';    
import { AllowAnyRole } from 'nest-keycloak-connect';
import { Observable, of } from 'rxjs';

@Controller('api')
export class TestController {
  @AllowAnyRole()
  @Get()
  findAll(): Observable<Array<string>> {
    console.info('api find all');
    return of(['today','is', 'sunny']);
  }
}

I don't think any of the above answer the question of how to read the cookie/header from nest.js api request and include it in all outgoing requests (say we call another api from this api and not the response of this api) .the second part of how to set the header is answered ,but the challenge here is to get the cookie and header in HttpModule.register or onModuleInit().

Related