NestJS Interceptors: Unable to set HTTP Headers on outgoing requests

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: '_ga=GA1.2.1433024000.1564057108; wfx_unq=AL2gejqqEGELJ5FQ; trafficManagerV2Token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVTJOakU0T0RJNE1nP
T0ifQ.eyJwZXJtaXRVcmwiOiJodHRwczovL2V2ZC5kYXRhLmRlbGZpLmNsb3VkLnNsYi1kcy5jb20vIiwiY291bnRyeUNvZGUiOiJJTiIsImlzcyI6ImNmcy10cmFmZmljLW1hbmFnZXIiLCJpYXQiOjE1NjYyNzM3ND
gsImV4cCI6MTU2NjI4ODE0OCwiYXVkIjoiaHR0cHM6Ly9ldmQuZGF0YS5kZWxmaS5jbG91ZC5zbGItZHMuY29tLyJ9.QHvZGR4DXdGpsWWNCnypPFttaBlpBCBSvy2N_Z0mgSD6W86g4f61GhO2XzFyIm7P20qAjkXHl
3CIo8R66wtYQqMIAOEd2BPcJVnKg9vdt2kxd1Fhk66BWTFd_xtTdyEgcwMuCmEkYEeFK1_cXrlbeGYpaRiXD6w6K1_2U1Wxtbu82BNp7R4eAuiLRbbLBdsuPgLwXsOI8YpFTMdpiUDMnZnTfw-Fr2F93KMzHKTswLy0y
QZVPtONj8BwXDPf15s2vLiTyzgof4ByM7O_eBIbBDse5tFufBXFABnr709Oi6AKUGMeVKsgwCo1d1Yxs7MR6nbNmyG3rFxKzhk5Xxehzw; x-origin-country=IN; stoken=eyJ0eXAiOiJKV1QiLCJhbGciOiJSU
zI1NiIsImtpZCI6Ik1UVTRORGcxTWpFeU1BPT0ifQ.eyJzdWIiOiJycHJhYmh1N0BzbGIuY29tIiwiaXNzIjoic2F1dGgtcHJldmlldy5zbGIuY29tIiwiYXVkIjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC
5jb20iLCJpYXQiOjE1ODQ5Mzk3MjYsImV4cCI6MTU4NTAyNjEyNiwicHJvdmlkZXIiOiJzbGIuY29tIiwiY2xpZW50IjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJ1c2VyaWQiOiJycHJhYmh1N0
BzbGIuY29tIiwiZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwiYXV0aHoiOiIiLCJsYXN0bmFtZSI6IlByYWJodSIsImZpcnN0bmFtZSI6IlJ1c2hpa2VzaCBTdWJoYXNoIiwiY291bnRyeSI6IiIsImNvbXBhbnkiOi
IiLCJqb2J0aXRsZSI6IiIsInN1YmlkIjoiRjBfSUMxSjl4SHBaSGVUbnVBaWRCYVhtdzI1YmxuOUhYSXIwMnNscW8wTSIsImlkcCI6Im8zNjUiLCJoZCI6InNsYi5jb20iLCJkZXNpZCI6InJwcmFiaHU3LXNsYi1jb2
0tNWZkODc5NzZAZGVzaWQuZGVsZmkuc2xiLmNvbSIsImNvbnRhY3RfZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwicnRfaGFzaCI6IlAzUG1yRXd5WExCR1VwTi05TTdybEEifQ.Z61iRRoS7J1IpF_V_rWLcrgeaSf
QyZG3K5vU4jps_LqB3VkPSvjHXLdv7Ga_LLPI_v2J-WFityHVBnYxLEzKmOuNc_jToPwmBqCmLLfSzIFGiJrFKby09ZbVoCCLHxjyUwB_Uc2VmWuYLce7oPpVFxelgRqnRjO3ymlPm65OvrR09fHiOlo52TULwbyyzeg
xzfodkl0eVTM7TURDi1RxGNHvw8Ghxt--AVIcgCT7hBDxA6w11D7Cr6fWBp1VpE2yawTESUWtZJn5tBmMZeZq2QobptNcuFdiAstQpvi_B5MqY1HY5LjVLOb2jAnEoCTl_gmEfyWr_aIKAFioK4YcQQ; _gid=GA1.2.
1341318697.1566283218; account-id=tenant1; _gat=1; traffic-manager-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVTRORGcxTWpFeU1BPT0ifQ.eyJwZXJtaXRVcmwi
OiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJjb3VudHJ5Q29kZSI6IklOIiwiaXNzIjoiY2ZzLXRyYWZmaWMtbWFuYWdlciIsImlhdCI6MTU4NDkzMTk5NywiZXhwIjoxNTg0OTQ2Mzk3LCJhdWQiOiJodHRwOi8vbG9j
YWxob3N0OjgwODAifQ.uVs2Uuy_Okzn0t3GPESH7cCR4OAb_ISr160JrydaKfkHogaKsuNEa7BI1vgQY8uywYle2P_sRaYT_FaoR9cF2iqHH7R7YHVdKEdNm_Gb2ji8nnLMjXORAMB78YtHt4SvnCNYrAxTqRPVhxRot
dQc6dcrVgzkxKxedDvnZTR81DfoOa00oeKrU7X62MSGMRDmz7TYLNxbaw0viJ-MlJ2AMHs_YhyRSHvmmG_5d0TVfNLBSnAiXlTH06iigVXfT5v-BbRukJJzaW1Pj30fde2G2ni0SZ8sK6nlrpu_0Tlu5-v1dKmdofhBs
qC8y8sCjZ8fTw4yZICl5AwPGZ4IOLkAeg',
[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 eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVTRORGcxTWpFeU1BPT0ifQ.eyJzdWIiOiJycHJhYmh1N0BzbGIuY29tIiwiaXNzIjoic2F1dGgtcHJldmlldy5z
bGIuY29tIiwiYXVkIjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJpYXQiOjE1ODQ5Mzk3MjYsImV4cCI6MTU4NTAyNjEyNiwicHJvdmlkZXIiOiJzbGIuY29tIiwiY2xpZW50IjoidGVzdC1zbGJk
ZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJ1c2VyaWQiOiJycHJhYmh1N0BzbGIuY29tIiwiZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwiYXV0aHoiOiIiLCJsYXN0bmFtZSI6IlByYWJodSIsImZpcnN0bmFtZSI6
IlJ1c2hpa2VzaCBTdWJoYXNoIiwiY291bnRyeSI6IiIsImNvbXBhbnkiOiIiLCJqb2J0aXRsZSI6IiIsInN1YmlkIjoiRjBfSUMxSjl4SHBaSGVUbnVBaWRCYVhtdzI1YmxuOUhYSXIwMnNscW8wTSIsImlkcCI6Im8z
NjUiLCJoZCI6InNsYi5jb20iLCJkZXNpZCI6InJwcmFiaHU3LXNsYi1jb20tNWZkODc5NzZAZGVzaWQuZGVsZmkuc2xiLmNvbSIsImNvbnRhY3RfZW1haWwiOiJycHJhYmh1N0BzbGIuY29tIiwicnRfaGFzaCI6IlAz
UG1yRXd5WExCR1VwTi05TTdybEEifQ.Z61iRRoS7J1IpF_V_rWLcrgeaSfQyZG3K5vU4jps_LqB3VkPSvjHXLdv7Ga_LLPI_v2J-WFityHVBnYxLEzKmOuNc_jToPwmBqCmLLfSzIFGiJrFKby09ZbVoCCLHxjyUwB_U
c2VmWuYLce7oPpVFxelgRqnRjO3ymlPm65OvrR09fHiOlo52TULwbyyzegxzfodkl0eVTM7TURDi1RxGNHvw8Ghxt--AVIcgCT7hBDxA6w11D7Cr6fWBp1VpE2yawTESUWtZJn5tBmMZeZq2QobptNcuFdiAstQpvi_B
5MqY1HY5LjVLOb2jAnEoCTl_gmEfyWr_aIKAFioK4YcQQ'
[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?


Solution 1:

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.

Solution 2:

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