serverless-express icon indicating copy to clipboard operation
serverless-express copied to clipboard

NestJS & Unable to determine event source based on event

Open mits87 opened this issue 2 years ago • 5 comments

Hi guys,

First of al thanks for the great plugin - helps a lot! Unfortunately I have a small problem when I'm trying to use it together with NestJS and GraphQL.

When I'm executing sls invoke local -f api I'm getting:

{
    "errorMessage": "Unable to determine event source based on event.",
    "errorType": "Error",
    "stackTrace": [
        "Error: Unable to determine event source based on event.",
        "    at getEventSourceNameBasedOnEvent (/Users/lolo/Sites/app/node_modules/@vendia/serverless-express/src/event-sources/utils.js:127:9)",
        "    at proxy (/Users/lolo/Sites/app/node_modules/@vendia/serverless-express/src/configure.js:38:51)",
        "    at handler (/Users/lolo/Sites/app/node_modules/@vendia/serverless-express/src/configure.js:99:12)",
        "    at handler (/Users/lolo/Sites/app/src/events/api.ts:35:10)"
    ]
}

My setup is similar to the example but of course much more advanced (I use additional GraphQL).

Below my setup:

serverless.yml

service: tmp-app

frameworkVersion: '3'
useDotenv: true

plugins:
  - serverless-offline

package:
  excludeDevDependencies: true
  individually: true

provider:
  name: aws
  runtime: nodejs18.x
  architecture: arm64
  region: ${opt:region, 'eu-central-1'}
  stage: ${opt:stage, 'dev'}
  memorySize: 2048
  versionFunctions: false
  logRetentionInDays: 1

functions:
  api:
    handler: ./src/events/api.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: ANY

./src/events/api.ts

import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@vendia/serverless-express';
import { NestFactory } from '@nestjs/core';
import { APIGatewayProxyEventV2, Callback, Context, Handler } from 'aws-lambda';
import express from 'express';

import { AppModule } from './app.module';

let server: Handler;

export const handler: Handler = async (
  event: APIGatewayProxyEventV2,
  context: Context,
  callback: Callback,
): Promise<Handler> => {
  if (!server) {
    const expressApp = express();

    const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp))
    await app.init();

    server = serverlessExpress({ app: expressApp });
  }

  return server(event, context, callback);
};

./app.module.ts

import { ApolloDriverConfig } from '@nestjs/apollo';
import { Module, NestModule } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

const IS_PROD = process.env.NODE_ENV === 'production';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: IS_PROD ? undefined : `${process.cwd()}/src/schema.graphql`,
      typePaths: IS_PROD ? ['./**/*.graphql'] : undefined,
      autoTransformHttpErrors: true,
      buildSchemaOptions: {
        // Refs. https://docs.nestjs.com/graphql/scalars#code-first
        dateScalarMode: 'timestamp',
      },
      introspection: true,
      installSubscriptionHandlers: true,
      sortSchema: true,
    }),
  ],
})
export class AppModule implements NestModule {}

./nest-cli.json

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true,
    "assets": [
      { "include": "**/*.graphql", "watchAssets": true }
    ],
    "plugins": [
      {
        "name": "@nestjs/graphql",
        "options": {
          "introspectComments": true
        }
      }
    ]
  }
}

Any ideas what is wrong?

mits87 avatar Oct 05 '23 08:10 mits87

Any news here?

mits87 avatar Nov 02 '23 19:11 mits87

Same problem here, any news ? 🥲 @mits87 Did you find a solution ?

Raphael0010 avatar Nov 14 '23 15:11 Raphael0010

Temporary Solution

The error is from the file utils, i will work to fix this problem, now i just work in this, and on the node_modules i commented a function call.

serverless.yml

service: test-lambda
useDotenv: true

plugins:
  - "serverless-plugin-typescript"
  - serverless-plugin-optimize
  - serverless-offline

provider:
  name: aws
  runtime: nodejs20.x

functions:
  main:
    handler: src/lambda.handler
    events:
      - http:
          method: ANY
          path: /
          async: true
      - http:
          method: ANY
          path: "{any+}"
          async: true

lambda.ts

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';

import { AppModule } from './app.module';

let server: Handler;

async function bootstrap() {
  if (!server) {
    const nestApp = await NestFactory.create(AppModule);
    nestApp.useGlobalPipes(new ValidationPipe());
    nestApp.use(cookieParser());
    await nestApp.init();

    const app = nestApp.getHttpAdapter().getInstance();
    server = serverlessExpress({ app });
  }
  return server;
}

export const handler: Handler = async (
  event,
  context: Context,
  callback: Callback,
) => {
  event = {
    ...event,
    path: event.requestPath,
    httpMethod: event.method,
    requestContext: {},
  };
  server = server ?? (await bootstrap());
  return await server(event, context, callback);
};

utils.js on node_modules commented

const url = require('url')

function getPathWithQueryStringParams ({
  event,
  query = event.multiValueQueryStringParameters,
  // NOTE: Use `event.pathParameters.proxy` if available ({proxy+}); fall back to `event.path`
  path = (event.pathParameters && event.pathParameters.proxy && `/${event.pathParameters.proxy}`) || event.path,
  // NOTE: Strip base path for custom domains
  stripBasePath = '',
  replaceRegex = new RegExp(`^${stripBasePath}`)
}) {
  return url.format({
    pathname: path.replace(replaceRegex, ''),
    query
  })
}

function getEventBody ({
  event,
  body = event.body,
  isBase64Encoded = event.isBase64Encoded
}) {
  return Buffer.from(body, isBase64Encoded ? 'base64' : 'utf8')
}

function getRequestValuesFromEvent ({
  event,
  method = event.httpMethod,
  path = getPathWithQueryStringParams({ event })
}) {
  let headers = {}

  if (event.multiValueHeaders) {
    headers = getCommaDelimitedHeaders({ headersMap: event.multiValueHeaders, lowerCaseKey: true })
  } else if (event.headers) {
    headers = event.headers
  }

  let body = event.body

  // if (event.body) {
  //   body = getEventBody({ event })
  //   const { isBase64Encoded } = event
  //   headers['content-length'] = Buffer.byteLength(body, isBase64Encoded ? 'base64' : 'utf8')
  // }

  const remoteAddress = (event && event.requestContext && event.requestContext.identity && event.requestContext.identity.sourceIp) || ''

  return {
    method,
    headers,
    body,
    remoteAddress,
    path
  }
}

function getMultiValueHeaders ({ headers }) {
  const multiValueHeaders = {}

  Object.entries(headers).forEach(([headerKey, headerValue]) => {
    const headerArray = Array.isArray(headerValue) ? headerValue.map(String) : [String(headerValue)]

    multiValueHeaders[headerKey.toLowerCase()] = headerArray
  })

  return multiValueHeaders
}

function getEventSourceNameBasedOnEvent ({
  event
}) {
  if (event.requestContext && event.requestContext.elb) return 'AWS_ALB'
  if (event.Records) {
    const eventSource = event.Records[0] ? event.Records[0].EventSource || event.Records[0].eventSource : undefined
    if (eventSource === 'aws:sns') {
      return 'AWS_SNS'
    }
    if (eventSource === 'aws:dynamodb') {
      return 'AWS_DYNAMODB'
    }
    if (eventSource === 'aws:sqs') {
      return 'AWS_SQS'
    }
    if (eventSource === 'aws:kinesis') {
      return 'AWS_KINESIS_DATA_STREAM'
    }
    return 'AWS_LAMBDA_EDGE'
  }
  if (event.requestContext) {
    return event.version === '2.0' ? 'AWS_API_GATEWAY_V2' : 'AWS_API_GATEWAY_V1'
  }
  if (event.traceContext) {
    const functionsExtensionVersion = process.env.FUNCTIONS_EXTENSION_VERSION

    if (!functionsExtensionVersion) {
      console.warn('The environment variable \'FUNCTIONS_EXTENSION_VERSION\' is not set. Only the function runtime \'~3\' is supported.')
    } else if (functionsExtensionVersion === '~3') {
      return 'AZURE_HTTP_FUNCTION_V3'
    } else if (functionsExtensionVersion === '~4') {
      return 'AZURE_HTTP_FUNCTION_V4'
    } else {
      console.warn('The function runtime \'' + functionsExtensionVersion + '\' is not supported. Only \'~3\' and \'~4\' are supported.')
    }
  }
  if (
    event.version &&
    event.version === '0' &&
    event.id &&
    event['detail-type'] &&
    event.source &&
    event.source.startsWith('aws.') && // Might need to adjust this for "Partner Sources", e.g. Auth0, Datadog, etc
    event.account &&
    event.time &&
    event.region &&
    event.resources &&
    Array.isArray(event.resources) &&
    event.detail &&
    typeof event.detail === 'object' &&
    !Array.isArray(event.detail)
  ) {
    // AWS doesn't have a defining Event Source here, so we're being incredibly selective on the structure
    // Ref: https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents.html
    return 'AWS_EVENTBRIDGE'
  }

  throw new Error('Unable to determine event source based on event.')
}

function getCommaDelimitedHeaders ({ headersMap, separator = ',', lowerCaseKey = false }) {
  const commaDelimitedHeaders = {}

  Object.entries(headersMap)
    .forEach(([headerKey, headerValue]) => {
      const newKey = lowerCaseKey ? headerKey.toLowerCase() : headerKey
      if (Array.isArray(headerValue)) {
        commaDelimitedHeaders[newKey] = headerValue.join(separator)
      } else {
        commaDelimitedHeaders[newKey] = headerValue
      }
    })

  return commaDelimitedHeaders
}

const emptyResponseMapper = () => {}

const parseCookie = (str) =>
  str.split(';')
    .map((v) => v.split('='))
    .reduce((acc, v) => {
      if (!v[1]) {
        return acc
      }
      acc[decodeURIComponent(v[0].trim().toLowerCase())] = decodeURIComponent(v[1].trim())
      return acc
    }, {})

module.exports = {
  getPathWithQueryStringParams,
  getRequestValuesFromEvent,
  getMultiValueHeaders,
  getEventSourceNameBasedOnEvent,
  getEventBody,
  getCommaDelimitedHeaders,
  emptyResponseMapper,
  parseCookie
}

hernandemonteiro avatar Nov 22 '23 23:11 hernandemonteiro

@hernandemonteiro thanks for that, waiting for a proper fix :)

mits87 avatar Jan 05 '24 15:01 mits87

AFAIK there is a fix but just when using {+proxy} mapping on AWS API Gateway.

klutzer avatar May 10 '24 21:05 klutzer