aws-lambda-fastify icon indicating copy to clipboard operation
aws-lambda-fastify copied to clipboard

Notes on Decorating Request when reusing Fastify instance

Open AlexStansfield opened this issue 3 years ago • 9 comments

Prerequisites

  • [X] I have written a descriptive issue title
  • [X] I have searched existing issues to ensure the issue has not already been raised

Issue

Not really a bug, but just might be worth adding a note in the readme.

The new feature to decorate the request with event and context data introduces errors if you are caching the fastify instance in order to save time bootstrapping.

For example I have a NestJS application I run on lambda, to avoid having to build the app every request it keeps it in memory until the lambda instance goes down.

let cachedNestApp: NestApp;

export const handler: Handler = async (
  event: APIGatewayProxyEvent,
  context: Context,
): Promise<APIGatewayProxyResult> => {
  if (!cachedNestApp) {
    cachedNestApp = await bootstrap();
  }

  const proxy = awsLambdaFastify(cachedNestApp.instance);
  return proxy(event, context);
};

I upgraded to the latest version of aws-lambda-fastify from 1.7.1 and the new decoration by default means that the first request works fine but afterwards every request errors:

{
    "errorType": "FastifyError",
    "errorMessage": "The decorator 'awsLambda' has been added after start!",
    "code": "FST_ERR_DEC_AFTER_START",
    "name": "FastifyError",
    "message": "The decorator 'awsLambda' has been added after start!",
    "statusCode": 500,
    "stack": [
        "FastifyError: The decorator 'awsLambda' has been added after start!",
        "    at assertNotStarted (/var/task/node_modules/fastify/lib/decorate.js:127:11)",
        "    at Object.decorateRequest (/var/task/node_modules/fastify/lib/decorate.js:119:3)",
        "    at module.exports (/var/task/node_modules/aws-lambda-fastify/index.js:9:9)",
        "    at Runtime.handler (/var/task/src/aws-lambda-fastify.js:54:19)",
        "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
    ]
}

I think this is expected behaviour, but just might come as a surprise to some who might upgrade and run into the same issues so i thought it might be worth noting in the readme.

AlexStansfield avatar Dec 27 '21 04:12 AlexStansfield

If you really want to cache the nest instance, you probably also need to cache the awsLambdaFastify(cachedNestApp.instance); proxy.

adrai avatar Dec 27 '21 07:12 adrai

I'll give it a shot, for now I just turned off the decoration as not using it.

AlexStansfield avatar Dec 27 '21 08:12 AlexStansfield

I think you just need to cache the proxy, not the nest app.

mcollina avatar Dec 27 '21 08:12 mcollina

Thanks, I experience the exact same issue with a similar app. I'll also try to disable the decorators and see if this is enough for me.

floric avatar Jan 01 '22 19:01 floric

Strangely, I'm seeing issues with NestJS even with a cached proxy instance. Mine is a different error though:

2022-01-04T00:08:28.566Z	ed7873b9-5086-42b7-9a14-79b0b28b6fb7	ERROR	Uncaught Exception 	
{
    "errorType": "TypeError",
    "errorMessage": "Cannot read property 'event' of undefined",
    "stack": [
        "TypeError: Cannot read property 'event' of undefined",
        "    at genReqId (/var/task/dist/internal-lambda.js:8:903277)",
        "    at Object.X [as handler] (/var/task/dist/internal-lambda.js:8:1006807)",
        "    at g.lookup (/var/task/dist/internal-lambda.js:8:360800)",
        "    at /var/task/dist/internal-lambda.js:8:29516",
        "    at p.prepare (/var/task/dist/internal-lambda.js:8:33476)",
        "    at m (/var/task/dist/internal-lambda.js:8:29501)",
        "    at y (/var/task/dist/internal-lambda.js:8:29797)",
        "    at h (/var/task/dist/internal-lambda.js:8:29331)",
        "    at /var/task/dist/internal-lambda.js:8:951755",
        "    at i (/var/task/dist/internal-lambda.js:8:953153)"
    ]
}

Code looks like:

let cachedProxy: PromiseHandler

type LambdaFastifyRequest = FastifyRequest & { awsLambda: { event: APIGatewayProxyEvent; context: Context } }
...
const bootstrap = async () => {
  const app = await NestFactory.create<NestFastifyApplication>(
    module,
    new FastifyAdapter({
      logger: true,
      genReqId: (request: unknown): string => {
        return (request as LambdaFastifyRequest).awsLambda.event.requestContext.requestId
      },
    }),
    {
      bufferLogs: true,
    },
  )
...
}
...
export const handle = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
  if (!cachedProxy) {
    cachedProxy = awsLambdaFastify(await bootstrap(), { decorateRequest: true })
  }
  return cachedProxy(event, context)
}

This is using 2.0.2. It seems like NestJS isn't carrying through the decoration properly. I've had to switch back to the old "JSON serialization in headers" method for now.

adworacz avatar Jan 04 '22 00:01 adworacz

Can you provide a reproducible github repo example?

adrai avatar Jan 04 '22 05:01 adrai

Yeah I can look into doing so. It's going to take me a bit as I'm a bit swamped this week, but I'll put it on my list.

adworacz avatar Jan 04 '22 22:01 adworacz

This might be a very naive approach to cache the proxy instance, but it does seem to be working. I'm not sure about the cold start side effect of using the ready() method.

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
import 'reflect-metadata';
import { FastifyServerOptions, FastifyInstance, fastify } from 'fastify';
import {
  Context,
  APIGatewayProxyEvent,
  APIGatewayProxyResult,
} from 'aws-lambda';
import { Logger } from '@nestjs/common';

import awsLambdaFastify, {
  LambdaResponse,
  PromiseHandler,
} from 'aws-lambda-fastify';

interface NestApp {
  app: NestFastifyApplication;
  instance: FastifyInstance;
}

let cachedNestApp: NestApp;
let cachedProxy: PromiseHandler<unknown, LambdaResponse>;

async function bootstrapServer(): Promise<NestApp> {
  const serverOptions: FastifyServerOptions = { logger: true };
  const instance: FastifyInstance = fastify(serverOptions);
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(instance),
    { logger: !process.env.AWS_EXECUTION_ENV ? new Logger() : console },
  );
  app.setGlobalPrefix(process.env.API_PREFIX);
  await app.init();
  return { app, instance };
}

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context,
): Promise<APIGatewayProxyResult> => {
  if (!cachedNestApp) {
    cachedNestApp = await bootstrapServer();
  }
  if (!cachedProxy) {
    cachedProxy = awsLambdaFastify(cachedNestApp.instance, {
      decorateRequest: true,
    });
    await cachedNestApp.instance.ready();
  }
  return cachedProxy(event, context);
};

yesid-bocanegra avatar Feb 23 '22 17:02 yesid-bocanegra

I would recommend to use ESM and move the two await to be top-level awaits and just use the proxy as your handler.

mcollina avatar Feb 23 '22 17:02 mcollina