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:
+51
-10
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-9
@@ -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,
|
||||
|
||||
+6
@@ -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,
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
+66
-7
@@ -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;
|
||||
|
||||
+6
@@ -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:
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user