Bbbbob
Bbbbob

Reputation: 495

NestJS: Receive form-data in Guards?

I'm looking to see form-data in my NestJS Guards. I've followed the tutorial, however, I'm not seeing the request body for my form-data input. I do see the body once I access a route within my controller, however.

Here's some code snippets of what I'm working with:

module.ts


...

@Module({
  imports: [
    MulterModule.register({
      limits: { fileSize: MULTER_UPLOAD_FILESIZE_BYTES },
    }),
  ],
  controllers: [MainController],
  providers: [
    MainService,
    AuthGuard,
  ],
})
...

AuthGuard.ts


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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest(); // body is empty if form-data is used
    return true;
  }
}
MainController.ts


...

@Post("/upload")
@UseInterceptors(AnyFilesInterceptor())
@UseGuards(AuthGuard)
  async upload(
    @Body() body: UploadDTO,
    @UploadedFiles() files: any[]
  ): Promise<any> {
    console.log(body) // works as expected, whether form-data is used or not
    ...
  }
...

Any feedback would be greatly appreciated!

Upvotes: 1

Views: 8009

Answers (2)

Charles Alleman
Charles Alleman

Reputation: 13

Posting my solution in-case it helps other devs dealing with the same issue.

To start, I created a middleware to handle the conversion of the multipart form data request. You could also inline this in to your guard if you only have one or two. Much of this code is plagiarised from the source code, and is not fully tested:

const multerExceptions = {
    LIMIT_PART_COUNT: 'Too many parts',
    LIMIT_FILE_SIZE: 'File too large',
    LIMIT_FILE_COUNT: 'Too many files',
    LIMIT_FIELD_KEY: 'Field name too long',
    LIMIT_FIELD_VALUE: 'Field value too long',
    LIMIT_FIELD_COUNT: 'Too many fields',
    LIMIT_UNEXPECTED_FILE: 'Unexpected field',
}

function transformException(error: Error | undefined) {
    if (!error || error instanceof HttpException) {
        return error
    }
    switch (error.message) {
        case multerExceptions.LIMIT_FILE_SIZE:
            return new PayloadTooLargeException(error.message)
        case multerExceptions.LIMIT_FILE_COUNT:
        case multerExceptions.LIMIT_FIELD_KEY:
        case multerExceptions.LIMIT_FIELD_VALUE:
        case multerExceptions.LIMIT_FIELD_COUNT:
        case multerExceptions.LIMIT_UNEXPECTED_FILE:
        case multerExceptions.LIMIT_PART_COUNT:
            return new BadRequestException(error.message)
    }
    return error
}

@Injectable()
export class MultipartMiddleware implements NestMiddleware {
    async use(req: Request, res: Response, next: NextFunction) {
        // Read multipart form data request
        // Multer modifies the request object
        await new Promise<void>((resolve, reject) => {
            multer().any()(req, res, (err: any) => {
                if (err) {
                    const error = transformException(err)
                    return reject(error)
                }
                resolve()
            })
        })

        next()
    }
}

Then, I applied the middleware conditionally to any routes which accept multipart form data:

@Module({
    controllers: [ExampleController],
    imports: [...],
    providers: [ExampleService],
})
export class ExampleModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer.apply(MultipartMiddleware).forRoutes({
            path: 'example/upload',
            method: RequestMethod.POST,
        })
    }
}

Finally, to get the uploaded files, you can reference req.files:

@Controller('example')
export class ExampleController {
    @Post('upload')
    upload(@Req() req: Request) {
        const files = req.files;
    }
}

I expanded this in my own codebase with some additional supporting decorators:

export const UploadedAttachment = createParamDecorator(
    (data: unknown, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest()
        return request.files?.[0]
    }
)

export const UploadedAttachments = createParamDecorator(
    (data: unknown, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest()
        return request.files
    }
)

Which ends up looking like:

@Controller('example')
export class ExampleController {
    @Post('upload')
    upload(@UploadedAttachments() files: Express.Multer.File[]) {
        ...
    }
}

Upvotes: 1

Hastou
Hastou

Reputation: 41

NestJS guards are always executed before any middleware. You can use multer manually on the request object you get from the context.

import * as multer from 'multer'
...
async canActivate(context: ExecutionContext): Promise<boolean> {
  const request: Request = context.switchToHttp().getRequest();
  const postMulterRequest = await new Promise((resolve, reject) => {
    multer().any()(request, {}, function(err) {
      if (err) reject(err);
      resolve(request);
    });
  });
  // postMulterRequest has a completed body
  return true;
}

If you want to use the @UploadedFiles decorator, you need to clone the request object before modifying it in your guard.

Of course you need to have installed the multer module with:

npm install multer

Upvotes: 4

Related Questions