Search code examples
typescriptnestjsclass-validator

Create a Header Custom Validation with NestJS and class-validator


I have been working to validate a request using the class-validator, and NestJS validation plus trying to validate the header contents.
My basic interfaces are all working, but now I am trying to compare some header field data the same way.

I had this question about the custom decorator to try to handle the headers, but the solution to that question, will return the one header. I want to be able to handle them all, similar to how all the body() data is processed.

I need to be able to create a custom decorator for extracting the header fields, and being able to pass them into the class-validator DTO.

For Instance, I want to validate three header fields, such as:

User-Agent = 'Our Client Apps'
Content-Type = 'application/json'
traceabilityId = uuid

There are more fields, but if I can get this going, then I can extrapolate out the rest. I have a simple controller example:

@Controller(/rest/package)
export class PackageController {

    constructor(
        private PackageData_:PackageService
    )
    { }

    ...

    @Post('inquiry')
    @HttpCode(HttpStatus.OK)        // Not creating data, but need body, so return 200 OK
    async StatusInquiry(
        @RequestHeader() HeaderInfo:HeadersDTO,     // This should be the Headers validation using the decorator from the question above.

I am trying to validate that the headers of the request contain some specific data, and I am using NestJS. I found this information. While this is what I want to do, and it looks proper, the ClassType reference does not exist, and I am not sure what to use instead.

From the example, the decorator is referring to.

request-header.decorator.ts

export interface iError {
    statusCode:number;
    messages:string[];
    error:string;
}

export const RequestHeader = createParamDecorator(
async (value:  any, ctx: ExecutionContext) => {

    // extract headers
    const headers = ctx.switchToHttp().getRequest().headers;

    // Convert headers to DTO object
    const dto = plainToClass(value, headers, { excludeExtraneousValues: true });

    // Validate
    const errors: ValidationError[] = await validate(dto);

    if (errors.length > 0) {
        let ErrorInfo:IError = {
            statusCode: HttpStatus.BAD_REQUEST,
            error: 'Bad Request',
            message: new Array<string>()
        };
        
        errors.map(obj => { 
            AllErrors = Object.values(obj.constraints);    
            AllErrors.forEach( (OneError) => {
            OneError.forEach( (Key) => {
                ErrorInfo.message.push(Key);
            });
        });

        // Your example, but wanted to return closer to how the body looks, for common error parsing
        //Get the errors and push to custom array
        // let validationErrors = errors.map(obj => Object.values(obj.constraints));
        throw new HttpException(`${ErrorInfo}`, HttpStatus.BAD_REQUEST);
    }

    // return header dto object
    return dto;
},

I am having trouble generically mapping the constraints into a string array.

My HeadersDTO.ts:

import { Expose } from 'class-transformer';
import { Equals, IsIn, IsString } from 'class-validator';
export class HeadersDTO {

    @IsString()
    @Equals('OurApp')
    @Expose({ name: 'user-agent' })
    public readonly 'user-agent':string;

    @IsString() 
    @IsIn(['PRODUCTION', 'TEST'])
    public readonly operationMode:string;
}

Headers being sent via Postman for the request:

Content-Type:application/json
operationMode:PRODUCTION
Accept-Language:en

Solution

  • I have just tested the following code and this is working. I think you are missing appropriate type over here,

        async StatusInquiry(
            @RequestHeader() HeaderInfo:HeadersDTO,
    

    You should have HeadersDTO passed in as param in RequestHeader Decorator this @RequestHeader(HeadersDTO) HeaderInfo:HeadersDTO,

    Then I have created customDecorator.ts like this,

        export const RequestHeader = createParamDecorator(
        //Removed ClassType<unknown>,, I don't think you need this here
        async (value:  any, ctx: ExecutionContext) => {
    
            // extract headers
            const headers = ctx.switchToHttp().getRequest().headers;
    
            // Convert headers to DTO object
            const dto = plainToClass(value, headers, { excludeExtraneousValues: true });
    
            // Validate
            const errors: ValidationError[] = await validate(dto);
            
            if (errors.length > 0) {
                //Get the errors and push to custom array
                let validationErrors = errors.map(obj => Object.values(obj.constraints));
                throw new HttpException(`Validation failed with following Errors: ${validationErrors}`, HttpStatus.BAD_REQUEST);
            }
    
            // return header dto object
            return dto;
        },
    );
    

    My HeadersDTO.ts file

        export class HeadersDTO 
        {
          @IsDefined()
          @Expose({ name: 'custom-header' })
          "custom-header": string; // note the param here is in double quotes
        }
    
    The reason i found this when I looked compiled TS file, it looks like this,
    
        class HeadersDTO {
        }
        tslib_1.__decorate([
            class_validator_1.IsDefined(),
            class_transformer_1.Expose({ name: 'custom-header' }),
            tslib_1.__metadata("design:type", String)
        ], HeadersDTO.prototype, "custom-header", void 0);
        exports.HeadersDTO = HeadersDTO;
    
    I get following error when i do not pass the header ,
    
        [
          ValidationError {
            target: HeadersDTO { 'custom-header': undefined },
            value: undefined,
            property: 'custom-header',
            children: [],
            constraints: { isDefined: 'custom-header should not be null or undefined' }
          }
        ]