Skip to content

Commit

Permalink
Handle errors in beforeInvoke/afterInvoke
Browse files Browse the repository at this point in the history
Related to #1848
  • Loading branch information
mnapoli committed Aug 14, 2024
1 parent 32f323e commit 515f62d
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 41 deletions.
90 changes: 55 additions & 35 deletions src/Runtime/LambdaRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,33 @@ public function processNextEvent(Handler | RequestHandlerInterface | callable $h
// Expose the context in an environment variable
$this->setEnv('LAMBDA_INVOCATION_CONTEXT', json_encode($context, JSON_THROW_ON_ERROR));

Bref::triggerHooks('beforeInvoke');
Bref::events()->beforeInvoke($handler, $event, $context);
try {
Bref::triggerHooks('beforeInvoke');
Bref::events()->beforeInvoke($handler, $event, $context);

$this->ping();
$this->ping();

try {
$result = $this->invoker->invoke($handler, $event, $context);

$this->sendResponse($context->getAwsRequestId(), $result);

Bref::events()->afterInvoke($handler, $event, $context, $result);
} catch (Throwable $e) {
$this->signalFailure($context->getAwsRequestId(), $e);

Bref::events()->afterInvoke($handler, $event, $context, null, $e);
try {
Bref::events()->afterInvoke($handler, $event, $context, null, $e);
} catch (Throwable $e) {
$this->logError($e, $context->getAwsRequestId());
}

return false;
}

// Any error in the afterInvoke hook happens after the response has been sent,
// we can no longer mark the invocation as failed. Instead we log the error.
try {
Bref::events()->afterInvoke($handler, $event, $context, $result);
} catch (Throwable $e) {
$this->logError($e, $context->getAwsRequestId());

return false;
}
Expand Down Expand Up @@ -195,33 +207,7 @@ private function sendResponse(string $invocationId, mixed $responseData): void
*/
private function signalFailure(string $invocationId, Throwable $error): void
{
$stackTraceAsArray = explode(PHP_EOL, $error->getTraceAsString());
$errorFormatted = [
'errorType' => get_class($error),
'errorMessage' => $error->getMessage(),
'stack' => $stackTraceAsArray,
];

if ($error->getPrevious() !== null) {
$previousError = $error;
$previousErrors = [];
do {
$previousError = $previousError->getPrevious();
$previousErrors[] = [
'errorType' => get_class($previousError),
'errorMessage' => $previousError->getMessage(),
'stack' => explode(PHP_EOL, $previousError->getTraceAsString()),
];
} while ($previousError->getPrevious() !== null);

$errorFormatted['previous'] = $previousErrors;
}

// Log the exception in CloudWatch
// We aim to use the same log format as what we can see when throwing an exception in the NodeJS runtime
// See https://github.com/brefphp/bref/pull/579
/** @noinspection JsonEncodingApiUsageInspection */
echo $invocationId . "\tInvoke Error\t" . json_encode($errorFormatted) . PHP_EOL;
$this->logError($error, $invocationId);

/**
* Send an "error" Lambda response (see https://github.com/brefphp/bref/pull/1483).
Expand All @@ -238,7 +224,7 @@ private function signalFailure(string $invocationId, Throwable $error): void
$this->postJson($url, [
'errorType' => get_class($error),
'errorMessage' => $error->getMessage(),
'stackTrace' => $stackTraceAsArray,
'stackTrace' => explode(PHP_EOL, $error->getTraceAsString()),
]);
}
}
Expand Down Expand Up @@ -444,4 +430,38 @@ private function setEnv(string $name, string $value): void
throw new RuntimeException("Failed to set environment variable $name");
}
}

/**
* Log the exception in CloudWatch
* We aim to use the same log format as what we can see when throwing an exception in the NodeJS runtime
*
* @see https://github.com/brefphp/bref/pull/579
*/
private function logError(Throwable $error, string $invocationId): void
{
$stackTraceAsArray = explode(PHP_EOL, $error->getTraceAsString());
$errorFormatted = [
'errorType' => get_class($error),
'errorMessage' => $error->getMessage(),
'stack' => $stackTraceAsArray,
];

if ($error->getPrevious() !== null) {
$previousError = $error;
$previousErrors = [];
do {
$previousError = $previousError->getPrevious();
$previousErrors[] = [
'errorType' => get_class($previousError),
'errorMessage' => $previousError->getMessage(),
'stack' => explode(PHP_EOL, $previousError->getTraceAsString()),
];
} while ($previousError->getPrevious() !== null);

$errorFormatted['previous'] = $previousErrors;
}

/** @noinspection JsonEncodingApiUsageInspection */
echo $invocationId . "\tInvoke Error\t" . json_encode($errorFormatted) . PHP_EOL;
}
}
12 changes: 6 additions & 6 deletions tests/Runtime/LambdaRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,15 +369,15 @@ public function handleEventBridge(EventBridgeEvent $event, Context $context): vo
public function test exceptions in beforeInvoke result in an invocation error()
{
Bref::events()->subscribe(new class extends BrefEventSubscriber {
public function beforeInvoke(...$params): void
public function beforeInvoke(mixed ...$params): void
{
throw new Exception('This is an exception in beforeInvoke');
}
});

$this->givenAnEvent([]);

$output = $this->runtime->processNextEvent(fn() => []);
$output = $this->runtime->processNextEvent(fn () => []);

$this->assertFalse($output);
$this->assertInvocationErrorResult('Exception', 'This is an exception in beforeInvoke');
Expand All @@ -391,14 +391,14 @@ public function beforeInvoke(...$params): void
public function test a failure in afterInvoke after a success does not signal a failure()
{
Bref::events()->subscribe(new class extends BrefEventSubscriber {
public function afterInvoke(...$params): void
public function afterInvoke(mixed ...$params): void
{
throw new Exception('This is an exception in afterInvoke');
}
});

$this->givenAnEvent([]);
$output = $this->runtime->processNextEvent(fn() => []);
$output = $this->runtime->processNextEvent(fn () => []);

$this->assertFalse($output);
$this->assertErrorInLogs('Exception', 'This is an exception in afterInvoke');
Expand All @@ -415,14 +415,14 @@ public function afterInvoke(...$params): void
public function test a failure in afterInvoke after a failure does not crash the runtime()
{
Bref::events()->subscribe(new class extends BrefEventSubscriber {
public function afterInvoke(...$params): void
public function afterInvoke(mixed ...$params): void
{
throw new Exception('This is an exception in afterInvoke');
}
});

$this->givenAnEvent([]);
$output = $this->runtime->processNextEvent(fn() => throw new Exception('Invocation error'));
$output = $this->runtime->processNextEvent(fn () => throw new Exception('Invocation error'));

$this->assertFalse($output);
// The error response was already sent, it contains the handler error
Expand Down

0 comments on commit 515f62d

Please sign in to comment.