Fix silent failures in logic function route trigger execution (#19698)

## Summary

Route-triggered logic functions were returning empty 500 responses with
zero server-side logging when the Lambda build chain failed. This PR
makes those failures observable and returns meaningful HTTP responses to
API clients.

- **Observability** — Log errors (with stack traces) at each layer of
the execution chain: `LambdaDriver` (deps-layer fetch, SDK-layer fetch,
invocation), `LogicFunctionExecutorService`, and `RouteTriggerService`.
- **Typed exceptions** — Replace raw `throw error` sites with
`LogicFunctionException` carrying an appropriate code and
`userFriendlyMessage` (new codes: `LOGIC_FUNCTION_EXECUTION_FAILED`,
`LOGIC_FUNCTION_LAYER_BUILD_FAILED`).
- **Correct HTTP semantics** — `RouteTriggerService` maps inner
exception codes to the right `RouteTriggerExceptionCode` so
`LOGIC_FUNCTION_NOT_FOUND` returns 404 and `RATE_LIMIT_EXCEEDED` returns
429 (new code + filter case) instead of a generic 500.
- **User-facing messages** — Forward the inner
`CustomException.userFriendlyMessage` when wrapping into
`RouteTriggerException`, without leaking raw internal error text into
the public exception message.
- **Infra** — Bump Lambda ephemeral storage from 2048 to 4096 MB to
prevent `ENOSPC` errors during yarn install layer builds (root cause of
the original silent failures).
This commit is contained in:
Charles Bochet
2026-04-15 18:49:43 +02:00
committed by GitHub
parent 5eda10760c
commit 446c39d1c0
7 changed files with 159 additions and 26 deletions
@@ -25,6 +25,7 @@ import {
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -64,7 +65,7 @@ const YARN_INSTALL_LAMBDA_MEMORY_MB = 1024;
const COMMON_LAYER_NAME_PREFIX = 'twenty-common-layer';
const BUILDER_LAMBDA_TIMEOUT_SECONDS = 60;
const BUILDER_LAMBDA_MEMORY_MB = 512;
const LAMBDA_EPHEMERAL_STORAGE_MB = 2048;
const LAMBDA_EPHEMERAL_STORAGE_MB = 4096;
const YARN_INSTALL_HANDLER_PATH = resolve(
__dirname,
join(
@@ -122,6 +123,7 @@ export interface LambdaDriverOptions extends LambdaClientConfig {
}
export class LambdaDriver implements LogicFunctionDriver {
private readonly logger = new Logger(LambdaDriver.name);
private lambdaClient: Lambda | undefined;
private assumeRoleCredentials:
| { accessKeyId: string; secretAccessKey: string; sessionToken: string }
@@ -1002,15 +1004,41 @@ export class LambdaDriver implements LogicFunctionDriver {
applicationUniversalIdentifier: string;
lambdaExecutor: GetFunctionCommandOutput | undefined;
}) {
const depsLayerArn = await this.getLayerArn({
flatApplication,
applicationUniversalIdentifier,
});
let depsLayerArn: string;
const sdkLayerArn = await this.ensureSdkLayer({
flatApplication,
applicationUniversalIdentifier,
});
try {
depsLayerArn = await this.getLayerArn({
flatApplication,
applicationUniversalIdentifier,
});
} catch (error) {
this.logger.error(
`Failed to get dependency layer for function ${flatLogicFunction.id}: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
throw new LogicFunctionException(
`Failed to get dependency layer for function '${flatLogicFunction.id}': ${error instanceof Error ? error.message : 'Unknown error'}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED,
);
}
let sdkLayerArn: string;
try {
sdkLayerArn = await this.ensureSdkLayer({
flatApplication,
applicationUniversalIdentifier,
});
} catch (error) {
this.logger.error(
`Failed to get SDK layer for function ${flatLogicFunction.id}: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
throw new LogicFunctionException(
`Failed to get SDK layer for function '${flatLogicFunction.id}': ${error instanceof Error ? error.message : 'Unknown error'}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED,
);
}
if (!isDefined(lambdaExecutor)) {
await this.createLambdaExecutor({
@@ -1158,13 +1186,26 @@ export class LambdaDriver implements LogicFunctionDriver {
status: LogicFunctionExecutionStatus.SUCCESS,
};
} catch (error) {
this.logger.error(
`Lambda invocation failed for function ${flatLogicFunction.id}: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
if (error instanceof ResourceNotFoundException) {
throw new LogicFunctionException(
`Function '${flatLogicFunction.id}' does not exist`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
throw error;
if (error instanceof LogicFunctionException) {
throw error;
}
throw new LogicFunctionException(
`Lambda invocation failed for function '${flatLogicFunction.id}': ${error instanceof Error ? error.message : 'Unknown error'}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED,
);
}
}
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import {
DEFAULT_API_KEY_NAME,
@@ -50,6 +50,8 @@ export enum LogicFunctionExecutionExceptionCode {
@Injectable()
export class LogicFunctionExecutorService {
private readonly logger = new Logger(LogicFunctionExecutorService.name);
constructor(
private readonly logicFunctionDriverFactory: LogicFunctionDriverFactory,
private readonly throttlerService: ThrottlerService,
@@ -87,14 +89,28 @@ export class LogicFunctionExecutorService {
const driver = this.logicFunctionDriverFactory.getCurrentDriver();
const resultLogicFunction = await driver.execute({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
payload,
env: envVariables,
timeoutMs: flatLogicFunction.timeoutSeconds * 1_000,
});
let resultLogicFunction: LogicFunctionExecuteResult;
try {
resultLogicFunction = await driver.execute({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
payload,
env: envVariables,
timeoutMs: flatLogicFunction.timeoutSeconds * 1_000,
});
} catch (error) {
this.logger.error(
`Logic function execution failed: ` +
`functionId=${logicFunctionId}, ` +
`workspaceId=${workspaceId}, ` +
`driver=${driver.constructor.name}: ` +
`${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
throw error;
}
await this.handleExecutionResult({
result: resultLogicFunction,
@@ -39,6 +39,12 @@ export class RouteTriggerRestApiExceptionFilter implements ExceptionFilter {
response,
403,
);
case RouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
response,
429,
);
case RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR:
return this.httpExceptionHandlerService.handleError(
exception as CustomException,
@@ -13,6 +13,7 @@ export enum RouteTriggerExceptionCode {
ROUTE_PATH_ALREADY_EXIST = 'ROUTE_PATH_ALREADY_EXIST',
FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION',
LOGIC_FUNCTION_EXECUTION_ERROR = 'LOGIC_FUNCTION_EXECUTION_ERROR',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
}
const getRouteTriggerExceptionUserFriendlyMessage = (
@@ -35,6 +36,8 @@ const getRouteTriggerExceptionUserFriendlyMessage = (
return msg`You do not have permission to perform this action.`;
case RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR:
return msg`Logic function execution failed.`;
case RouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED:
return msg`Too many requests. Please try again later.`;
default:
assertUnreachable(code);
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Request } from 'express';
@@ -14,11 +14,22 @@ import {
RouteTriggerExceptionCode,
} from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception';
import { buildLogicFunctionEvent } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/utils/build-logic-function-event.util';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
import {
LogicFunctionExecutionException,
LogicFunctionExecutionExceptionCode,
LogicFunctionExecutorService,
} from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
import { CustomException } from 'src/utils/custom-exception';
@Injectable()
export class RouteTriggerService {
private readonly logger = new Logger(RouteTriggerService.name);
constructor(
private readonly accessTokenService: AccessTokenService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
@@ -118,6 +129,28 @@ export class RouteTriggerService {
return authContext;
}
private mapErrorToRouteTriggerCode(
error: unknown,
): RouteTriggerExceptionCode {
if (error instanceof LogicFunctionExecutionException) {
switch (error.code) {
case LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
return RouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND;
case LogicFunctionExecutionExceptionCode.RATE_LIMIT_EXCEEDED:
return RouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED;
}
}
if (
error instanceof LogicFunctionException &&
error.code === LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND
) {
return RouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND;
}
return RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR;
}
async handle({
request,
httpMethod,
@@ -146,11 +179,37 @@ export class RouteTriggerService {
forwardedRequestHeaders: httpRouteSettings?.forwardedRequestHeaders ?? [],
});
const result = await this.logicFunctionExecutorService.execute({
logicFunctionId: logicFunction.id,
workspaceId: logicFunction.workspaceId,
payload: event,
});
let result;
try {
result = await this.logicFunctionExecutorService.execute({
logicFunctionId: logicFunction.id,
workspaceId: logicFunction.workspaceId,
payload: event,
});
} catch (error) {
if (error instanceof RouteTriggerException) {
throw error;
}
this.logger.error(
`Unexpected error executing logic function ${logicFunction.id}: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
);
const code = this.mapErrorToRouteTriggerCode(error);
throw new RouteTriggerException(
`Logic function execution failed for ${logicFunction.id}`,
code,
{
userFriendlyMessage:
error instanceof CustomException
? error.userFriendlyMessage
: undefined,
},
);
}
if (!isDefined(result)) {
return result;
@@ -14,6 +14,8 @@ export enum LogicFunctionExceptionCode {
LOGIC_FUNCTION_CREATE_FAILED = 'LOGIC_FUNCTION_CREATE_FAILED',
LOGIC_FUNCTION_COMPILATION_FAILED = 'LOGIC_FUNCTION_COMPILATION_FAILED',
LOGIC_FUNCTION_EXECUTION_TIMEOUT = 'LOGIC_FUNCTION_EXECUTION_TIMEOUT',
LOGIC_FUNCTION_EXECUTION_FAILED = 'LOGIC_FUNCTION_EXECUTION_FAILED',
LOGIC_FUNCTION_LAYER_BUILD_FAILED = 'LOGIC_FUNCTION_LAYER_BUILD_FAILED',
LOGIC_FUNCTION_DISABLED = 'LOGIC_FUNCTION_DISABLED',
LOGIC_FUNCTION_INVALID_SEED_PROJECT = 'LOGIC_FUNCTION_INVALID_SEED_PROJECT',
}
@@ -40,6 +42,10 @@ const getLogicFunctionExceptionUserFriendlyMessage = (
return msg`Function code failed to compile.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_TIMEOUT:
return msg`Function execution timed out.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED:
return msg`Function execution failed.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED:
return msg`Failed to build function dependencies.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED:
return msg`Logic function execution is disabled.`;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT:
@@ -29,6 +29,8 @@ export const logicFunctionGraphQLApiExceptionHandler = (error: any) => {
case LogicFunctionExceptionCode.LOGIC_FUNCTION_CODE_UNCHANGED:
case LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED:
case LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT:
case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED:
case LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED:
throw error;
case LogicFunctionExceptionCode.LOGIC_FUNCTION_COMPILATION_FAILED:
throw new UserInputError(error);