NestJS & Unable to determine event source based on event
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?
Any news here?
Same problem here, any news ? 🥲 @mits87 Did you find a solution ?
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 thanks for that, waiting for a proper fix :)
AFAIK there is a fix but just when using {+proxy} mapping on AWS API Gateway.