From c33308a72b05181e5acc17c63f6688c5a2a3bebb Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 00:51:49 -0400
Subject: [PATCH 1/8] feat: create propagate error to client error handle
middleware
---
.../propagate_error_to_client_error_handle.ts | 101 ++++++++++++++++++
.../propagate_error_to_client_error_handle.ts | 68 ++++++++++++
2 files changed, 169 insertions(+)
create mode 100644 src/middleware/propagate_error_to_client_error_handle.ts
create mode 100644 test/middleware/propagate_error_to_client_error_handle.ts
diff --git a/src/middleware/propagate_error_to_client_error_handle.ts b/src/middleware/propagate_error_to_client_error_handle.ts
new file mode 100644
index 000000000..74101793c
--- /dev/null
+++ b/src/middleware/propagate_error_to_client_error_handle.ts
@@ -0,0 +1,101 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import {Request, Response, NextFunction, Express} from 'express';
+import {HandlerFunction} from '../functions';
+import {ILayer} from 'express-serve-static-core';
+
+/**
+ * Common properties that exists on Express object instances. Extracted by calling
+ * `Object.getOwnPropertyNames` on an express instance.
+ */
+const COMMON_EXPRESS_OBJECT_PROPERTIES = [
+ '_router',
+ 'use',
+ 'get',
+ 'post',
+ 'put',
+ 'delete',
+];
+
+/** The number of parameters on an express error handler. */
+const EXPRESS_ERROR_HANDLE_PARAM_LENGTH = 4;
+
+/** A express app error handle. */
+interface ErrorHandle {
+ (err: Error, req: Request, res: Response, next: NextFunction): any;
+}
+
+/**
+ * Express middleware to propagate framework errors to the user function error handle.
+ * This enables users to handle framework errors that would otherwise be handled by the
+ * default Express error handle. If the user function doesn't have an error handle,
+ * it falls back to the default Express error handle.
+ * @param userFunction - User handler function
+ */
+export const createPropagateErrorToClientErrorHandleMiddleware = (
+ userFunction: HandlerFunction
+): ErrorHandle => {
+ const userFunctionErrorHandle =
+ getFirstUserFunctionErrorHandleMiddleware(userFunction);
+
+ return function (
+ err: Error,
+ req: Request,
+ res: Response,
+ next: NextFunction
+ ) {
+ // Propagate error to user function error handle.
+ if (userFunctionErrorHandle) {
+ return userFunctionErrorHandle(err, req, res, next);
+ }
+
+ // Propagate error to default Express error handle.
+ return next();
+ };
+};
+
+/**
+ * Returns the first user handler function defined error handle, if available.
+ * @param userFunction - User handler function
+ */
+const getFirstUserFunctionErrorHandleMiddleware = (
+ userFunction: HandlerFunction
+): ErrorHandle | null => {
+ if (!isExpressApp(userFunction)) {
+ return null;
+ }
+
+ const middlewares: ILayer[] = (userFunction as Express)._router.stack;
+ for (const middleware of middlewares) {
+ if (
+ middleware.handle &&
+ middleware.handle.length === EXPRESS_ERROR_HANDLE_PARAM_LENGTH
+ ) {
+ return middleware.handle as unknown as ErrorHandle;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Returns if the user function contains common properties of an Express app.
+ * @param userFunction
+ */
+const isExpressApp = (userFunction: HandlerFunction): boolean => {
+ const userFunctionProperties = Object.getOwnPropertyNames(userFunction);
+ return COMMON_EXPRESS_OBJECT_PROPERTIES.every(prop =>
+ userFunctionProperties.includes(prop)
+ );
+};
diff --git a/test/middleware/propagate_error_to_client_error_handle.ts b/test/middleware/propagate_error_to_client_error_handle.ts
new file mode 100644
index 000000000..81d1074e8
--- /dev/null
+++ b/test/middleware/propagate_error_to_client_error_handle.ts
@@ -0,0 +1,68 @@
+import * as assert from 'assert';
+import * as sinon from 'sinon';
+import {NextFunction} from 'express';
+import {Request, Response} from '../../src';
+import {createPropagateErrorToClientErrorHandleMiddleware} from '../../src/middleware/propagate_error_to_client_error_handle';
+import * as express from 'express';
+
+describe('propagateErrorToClientErrorHandleMiddleware', () => {
+ let request: Request;
+ let response: Response;
+ let next: NextFunction;
+ let errListener = () => {};
+ let error: Error;
+ beforeEach(() => {
+ error = Error('Something went wrong!');
+ request = {
+ setTimeout: sinon.spy(),
+ on: sinon.spy(),
+ } as unknown as Request;
+ response = {
+ on: sinon.spy(),
+ } as unknown as Response;
+ next = sinon.spy();
+ errListener = sinon.spy();
+ });
+
+ it('user express app with error handle calls user function app error handle', () => {
+ const app = express();
+ app.use(
+ (
+ _err: Error,
+ _req: Request,
+ _res: Response,
+ _next: NextFunction
+ ): any => {
+ errListener();
+ }
+ );
+
+ const middleware = createPropagateErrorToClientErrorHandleMiddleware(app);
+ middleware(error, request, response, next);
+
+ assert.strictEqual((errListener as sinon.SinonSpy).called, true);
+ assert.strictEqual((next as sinon.SinonSpy).called, false);
+ });
+
+ it('user express app without error handle calls default express error handle', () => {
+ const app = express();
+
+ const middleware = createPropagateErrorToClientErrorHandleMiddleware(app);
+ middleware(error, request, response, next);
+
+ assert.strictEqual((errListener as sinon.SinonSpy).called, false);
+ assert.strictEqual((next as sinon.SinonSpy).called, true);
+ });
+
+ it('non-express user app calls default express error handle', () => {
+ const app = (_req: Request, res: Response) => {
+ res.send('Hello, World!');
+ };
+
+ const middleware = createPropagateErrorToClientErrorHandleMiddleware(app);
+ middleware(error, request, response, next);
+
+ assert.strictEqual((errListener as sinon.SinonSpy).called, false);
+ assert.strictEqual((next as sinon.SinonSpy).called, true);
+ });
+});
From 9ad6a088b7f4bce9464c313864a16623278a3313 Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 01:26:34 -0400
Subject: [PATCH 2/8] feat: add propagate errors to client cli option
---
src/options.ts | 23 +++++++++++++++++++-
src/testing.ts | 1 +
test/integration/legacy_event.ts | 1 +
test/options.ts | 37 ++++++++++++++++++++++++++++++++
4 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/src/options.ts b/src/options.ts
index a0e9c89fa..a3719f50a 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -15,7 +15,7 @@
import * as minimist from 'minimist';
import * as semver from 'semver';
import {resolve} from 'path';
-import {SignatureType, isValidSignatureType} from './types';
+import {isValidSignatureType, SignatureType} from './types';
/**
* Error thrown when an invalid option is provided.
@@ -60,6 +60,10 @@ export interface FrameworkOptions {
* Routes that should return a 404 without invoking the function.
*/
ignoredRoutes: string | null;
+ /**
+ * Whether or not to propagate framework errors to the client.
+ */
+ propagateFrameworkErrors: boolean;
}
/**
@@ -167,6 +171,18 @@ const ExecutionIdOption = new ConfigurableOption(
}
);
+const PropagateFrameworkErrorsOption = new ConfigurableOption(
+ 'propagate-framework-errors',
+ 'PROPAGATE_FRAMEWORK_ERRORS',
+ false,
+ x => {
+ return (
+ (typeof x === 'boolean' && x) ||
+ (typeof x === 'string' && x.toLowerCase() === 'true')
+ );
+ }
+);
+
export const helpText = `Example usage:
functions-framework --target=helloWorld --port=8080
Documentation:
@@ -191,6 +207,7 @@ export const parseOptions = (
SourceLocationOption.cliOption,
TimeoutOption.cliOption,
IgnoredRoutesOption.cliOption,
+ PropagateFrameworkErrorsOption.cliOption,
],
});
return {
@@ -202,5 +219,9 @@ export const parseOptions = (
printHelp: cliArgs[2] === '-h' || cliArgs[2] === '--help',
enableExecutionId: ExecutionIdOption.parse(argv, envVars),
ignoredRoutes: IgnoredRoutesOption.parse(argv, envVars),
+ propagateFrameworkErrors: PropagateFrameworkErrorsOption.parse(
+ argv,
+ envVars
+ ),
};
};
diff --git a/src/testing.ts b/src/testing.ts
index 2fda93c28..1e4298b14 100644
--- a/src/testing.ts
+++ b/src/testing.ts
@@ -57,5 +57,6 @@ export const getTestServer = (functionName: string): Server => {
sourceLocation: '',
printHelp: false,
ignoredRoutes: null,
+ propagateFrameworkErrors: false,
});
};
diff --git a/test/integration/legacy_event.ts b/test/integration/legacy_event.ts
index 231d0f62e..3a1fd72d3 100644
--- a/test/integration/legacy_event.ts
+++ b/test/integration/legacy_event.ts
@@ -41,6 +41,7 @@ const testOptions = {
sourceLocation: '',
printHelp: false,
ignoredRoutes: null,
+ propagateFrameworkErrors: false,
};
describe('Event Function', () => {
diff --git a/test/options.ts b/test/options.ts
index b62794851..cab3a7a4d 100644
--- a/test/options.ts
+++ b/test/options.ts
@@ -61,6 +61,7 @@ describe('parseOptions', () => {
enableExecutionId: false,
timeoutMilliseconds: 0,
ignoredRoutes: null,
+ propagateFrameworkErrors: false,
},
},
{
@@ -77,6 +78,7 @@ describe('parseOptions', () => {
'--timeout',
'6',
'--ignored-routes=banana',
+ '--propagate-framework-errors=true',
],
envVars: {},
expectedOptions: {
@@ -88,6 +90,7 @@ describe('parseOptions', () => {
enableExecutionId: false,
timeoutMilliseconds: 6000,
ignoredRoutes: 'banana',
+ propagateFrameworkErrors: true,
},
},
{
@@ -100,6 +103,7 @@ describe('parseOptions', () => {
FUNCTION_SOURCE: '/source',
CLOUD_RUN_TIMEOUT_SECONDS: '2',
IGNORED_ROUTES: '',
+ PROPAGATE_FRAMEWORK_ERRORS: 'true',
},
expectedOptions: {
port: '1234',
@@ -110,6 +114,7 @@ describe('parseOptions', () => {
enableExecutionId: false,
timeoutMilliseconds: 2000,
ignoredRoutes: '',
+ propagateFrameworkErrors: true,
},
},
{
@@ -125,6 +130,7 @@ describe('parseOptions', () => {
'--source=/source',
'--timeout=3',
'--ignored-routes=avocado',
+ '--propagate-framework-errors',
],
envVars: {
PORT: '4567',
@@ -133,6 +139,7 @@ describe('parseOptions', () => {
FUNCTION_SOURCE: '/somewhere/else',
CLOUD_RUN_TIMEOUT_SECONDS: '5',
IGNORED_ROUTES: 'banana',
+ PROPAGATE_FRAMEWORK_ERRORS: 'false',
},
expectedOptions: {
port: '1234',
@@ -143,6 +150,7 @@ describe('parseOptions', () => {
enableExecutionId: false,
timeoutMilliseconds: 3000,
ignoredRoutes: 'avocado',
+ propagateFrameworkErrors: false,
},
},
];
@@ -236,4 +244,33 @@ describe('parseOptions', () => {
});
});
});
+
+ it('default disable propagate framework errors', () => {
+ const options = parseOptions(['bin/node', '/index.js'], {});
+ assert.strictEqual(options.propagateFrameworkErrors, false);
+ });
+
+ it('disable propagate framework errors by cli flag', () => {
+ const options = parseOptions(
+ ['bin/node', '/index.js', '--propagate-framework-errors=false'],
+ {}
+ );
+ assert.strictEqual(options.propagateFrameworkErrors, false);
+ });
+
+ it('enable propagate framework errors by cli flag', () => {
+ const options = parseOptions(
+ ['bin/node', '/index.js', '--propagate-framework-errors=true'],
+ {}
+ );
+ assert.strictEqual(options.propagateFrameworkErrors, true);
+ });
+
+ it('disable propagate framework errors by env var', () => {
+ const envVars = {
+ PROPAGATE_FRAMEWORK_ERRORS: 'False',
+ };
+ const options = parseOptions(cliOpts, envVars);
+ assert.strictEqual(options.propagateFrameworkErrors, false);
+ });
});
From e20f8237405289c8336c09557f6b3e853d7d1c33 Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 01:28:29 -0400
Subject: [PATCH 3/8] fix: Fix gts errors
---
src/middleware/propagate_error_to_client_error_handle.ts | 1 +
test/middleware/propagate_error_to_client_error_handle.ts | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/src/middleware/propagate_error_to_client_error_handle.ts b/src/middleware/propagate_error_to_client_error_handle.ts
index 74101793c..a4cfc213c 100644
--- a/src/middleware/propagate_error_to_client_error_handle.ts
+++ b/src/middleware/propagate_error_to_client_error_handle.ts
@@ -33,6 +33,7 @@ const EXPRESS_ERROR_HANDLE_PARAM_LENGTH = 4;
/** A express app error handle. */
interface ErrorHandle {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
(err: Error, req: Request, res: Response, next: NextFunction): any;
}
diff --git a/test/middleware/propagate_error_to_client_error_handle.ts b/test/middleware/propagate_error_to_client_error_handle.ts
index 81d1074e8..3f6254d3d 100644
--- a/test/middleware/propagate_error_to_client_error_handle.ts
+++ b/test/middleware/propagate_error_to_client_error_handle.ts
@@ -28,10 +28,15 @@ describe('propagateErrorToClientErrorHandleMiddleware', () => {
const app = express();
app.use(
(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
_err: Error,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
_req: Request,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
_res: Response,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: NextFunction
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
): any => {
errListener();
}
From 2fc508c2bbb9567375e2d39e3f1d24bc5a204109 Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 02:41:32 -0400
Subject: [PATCH 4/8] feat: propagate framework errors to client when option
enabled
---
src/server.ts | 7 ++++
test/integration/http.ts | 87 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+)
diff --git a/src/server.ts b/src/server.ts
index 9a9b3acc3..87197afce 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -26,6 +26,7 @@ import {wrapUserFunction} from './function_wrappers';
import {asyncLocalStorageMiddleware} from './async_local_storage';
import {executionContextMiddleware} from './execution_context';
import {FrameworkOptions} from './options';
+import {createPropagateErrorToClientErrorHandleMiddleware} from './middleware/propagate_error_to_client_error_handle';
/**
* Creates and configures an Express application and returns an HTTP server
@@ -172,5 +173,11 @@ export function getServer(
app.post('/*', requestHandler);
}
+ if (options.propagateFrameworkErrors) {
+ const errorHandleMiddleware =
+ createPropagateErrorToClientErrorHandleMiddleware(userFunction);
+ app.use(errorHandleMiddleware);
+ }
+
return http.createServer(app);
}
diff --git a/test/integration/http.ts b/test/integration/http.ts
index b510fbaa8..0954e1455 100644
--- a/test/integration/http.ts
+++ b/test/integration/http.ts
@@ -18,6 +18,9 @@ import * as supertest from 'supertest';
import * as functions from '../../src/index';
import {getTestServer} from '../../src/testing';
+import {Request, Response, NextFunction} from 'express';
+import * as express from 'express';
+import {getServer} from '../../src/server';
describe('HTTP Function', () => {
let callCount = 0;
@@ -111,4 +114,88 @@ describe('HTTP Function', () => {
assert.strictEqual(callCount, test.expectedCallCount);
});
});
+
+ it('default error handler', async () => {
+ const app = express();
+ app.post('/foo', async (req, res) => {
+ res.send('Foo!');
+ });
+ app.use(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ (_err: Error, _req: Request, res: Response, _next: NextFunction) => {
+ res.status(500).send('Caught error!');
+ }
+ );
+ functions.http('testHttpFunction', app);
+ const malformedBody = '{"key": "value", "anotherKey": }';
+ const st = supertest(
+ getServer(app, {
+ port: '',
+ target: '',
+ sourceLocation: '',
+ signatureType: 'http',
+ printHelp: false,
+ enableExecutionId: false,
+ timeoutMilliseconds: 0,
+ ignoredRoutes: null,
+ propagateFrameworkErrors: false,
+ })
+ );
+ const resBody =
+ '<!DOCTYPE html>\n' +
+ '<html lang="en">\n' +
+ '<head>\n' +
+ '<meta charset="utf-8">\n' +
+ '<title>Error</title>\n' +
+ '</head>\n' +
+ '<body>\n' +
+ '<pre>SyntaxError: Unexpected token '}', ..."therKey": }" is not valid JSON<br> at JSON.parse (<anonymous>)<br> at parse (/Users/gregei/IdeaProjects/functions-framework-nodejs/node_modules/body-parser/lib/types/json.js:92:19)<br> at /Users/gregei/IdeaProjects/functions-framework-nodejs/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (node:async_hooks:211:14)<br> at invokeCallback (/Users/gregei/IdeaProjects/functions-framework-nodejs/node_modules/raw-body/index.js:238:16)<br> at done (/Users/gregei/IdeaProjects/functions-framework-nodejs/node_modules/raw-body/index.js:227:7)<br> at IncomingMessage.onEnd (/Users/gregei/IdeaProjects/functions-framework-nodejs/node_modules/raw-body/index.js:287:7)<br> at IncomingMessage.emit (node:events:518:28)<br> at IncomingMessage.emit (node:domain:552:15)<br> at endReadableNT (node:internal/streams/readable:1698:12)<br> at process.processTicksAndRejections (node:internal/process/task_queues:90:21)</pre>\n' +
+ '</body>\n' +
+ '</html>\n';
+
+ const response = await st
+ .post('/foo')
+ .set('Content-Type', 'application/json')
+ .send(malformedBody);
+
+ assert.strictEqual(response.status, 400);
+ assert.equal(response.text, resBody);
+ });
+
+ it('user application error handler', async () => {
+ const app = express();
+ const resBody = 'Caught error!';
+ app.post('/foo', async (req, res) => {
+ res.send('Foo!');
+ });
+ app.use(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ (_err: Error, _req: Request, res: Response, _next: NextFunction) => {
+ res.status(500).send(resBody);
+ }
+ );
+ functions.http('testHttpFunction', app);
+ const malformedBody = '{"key": "value", "anotherKey": }';
+ const st = supertest(
+ getServer(app, {
+ port: '',
+ target: '',
+ sourceLocation: '',
+ signatureType: 'http',
+ printHelp: false,
+ enableExecutionId: false,
+ timeoutMilliseconds: 0,
+ ignoredRoutes: null,
+ propagateFrameworkErrors: true,
+ })
+ );
+
+ const response = await st
+ .post('/foo')
+ .set('Content-Type', 'application/json')
+ .send(malformedBody);
+
+ assert.strictEqual(response.status, 500);
+ assert.equal(response.text, resBody);
+ });
});
From ed2668ecbe9a21f37e9bbd998156890b306da6a5 Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 02:48:07 -0400
Subject: [PATCH 5/8] chore: update readme to contain propagate framework
errors cli option
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 88144492e..f5f7d245f 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,7 @@ ignored.
| `--source` | `FUNCTION_SOURCE` | The path to the directory of your function. Default: `cwd` (the current working directory) |
| `--log-execution-id`| `LOG_EXECUTION_ID` | Enables execution IDs in logs, either `true` or `false`. When not specified, default to disable. Requires Node.js 13.0.0 or later. |
| `--ignored-routes`| `IGNORED_ROUTES` | A route expression for requests that should not be routed the function. An empty 404 response will be returned. This is set to `/favicon.ico|/robots.txt` by default for `http` functions. |
+| `--propagate-framework-errors` | `PROPAGATE_FRAMEWORK_ERRORS` | Enables propagating framework errors to the client application error handler, either `true` or `false`. When not specified, default to disable.|
You can set command-line flags in your `package.json` via the `start` script.
For example:
From eb6184204e4c57c1dcdb4d693687535cf6c45acb Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 03:51:51 -0400
Subject: [PATCH 6/8] chore: Add docs for propagating framework errors
---
docs/README.md | 1 +
docs/propagate-internal-framework-errors.md | 81 +++++++++++++++++++++
2 files changed, 82 insertions(+)
create mode 100644 docs/propagate-internal-framework-errors.md
diff --git a/docs/README.md b/docs/README.md
index af449ae46..8fae5673f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -9,6 +9,7 @@ This directory contains advanced docs around the Functions Framework.
- [Running and Deploying Docker Containers](docker.md)
- [Writing a Function in Typescript](typescript.md)
- [ES Modules](esm/README.md)
+- [Propagate internal framework errors](propagate-internal-framework-errors.md)
## Generated Docs
diff --git a/docs/propagate-internal-framework-errors.md b/docs/propagate-internal-framework-errors.md
new file mode 100644
index 000000000..925c08faa
--- /dev/null
+++ b/docs/propagate-internal-framework-errors.md
@@ -0,0 +1,81 @@
+# Propagate Internal Framework Errors
+
+The Functions Framework normally sends express level errors to the default express error handler which sends the error to the calling client with an optional stack trace if in a non-prod environment.
+
+## Example
+
+```ts
+const app = express();
+
+app.post("/", (req, res) => {
+...
+});
+
+// User error handler
+app.use((err, req, res, next) => {
+ logger.log(err);
+ res.send("Caught error!");
+});
+
+functions.http("helloWorld, app);
+```
+
+```ts
+// Post request with bad JSON
+http.post("/", "{"id": "Hello}");
+```
+
+Default express error handler:
+
+```
+SyntaxError: Expected double-quoted property name in JSON at position 20 (line 3 column 1)
+ at JSON.parse (<anonymous>)
+ at parse (functions-framework-nodejs/node_modules/body-parser/lib/types/json.js:92:19)
+ at functions-framework-nodejs/node_modules/body-parser/lib/read.js:128:18
+ at AsyncResource.runInAsyncScope (node:async_hooks:211:14)
+ at invokeCallback (functions-framework-nodejs/node_modules/raw-body/index.js:238:16)
+ at done (functions-framework-nodejs/node_modules/raw-body/index.js:227:7)
+ at IncomingMessage.onEnd (functions-framework-nodejs/node_modules/raw-body/index.js:287:7)
+ at IncomingMessage.emit (node:events:518:28)
+ at endReadableNT (node:internal/streams/readable:1698:12)
+ at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
+```
+
+## Propagating Errors
+
+If you want to propgate internal express level errors to your application, enabling the propagate option and defining a custom error handler will allow your application to receive errors:
+
+1. In your `package.json`, specify `--propagate-framework-errors=true"` for the `functions-framework`:
+
+```sh
+{
+ "scripts": {
+ "start": "functions-framework --target=helloWorld --propagate-framework-errors=true"
+ }
+}
+```
+
+2. Define a express error handler:
+
+```ts
+const app = express();
+
+// User error handler
+app.use((err, req, res, next) => {
+ logger.log(err);
+ res.send("Caught error!");
+});
+```
+
+Now your application will receive internal express level errors!
+
+```ts
+// Post request with bad JSON
+http.post("/", "{"id": "Hello}");
+```
+
+The custom error handler logic executes:
+
+```
+Caught error!
+```
\ No newline at end of file
From 90ea72e6793614c75d9b93799e8e590e199dac03 Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Sun, 30 Mar 2025 12:25:06 -0400
Subject: [PATCH 7/8] fix: Inject user function error handle middleware chain
into framework instead of just the middleware
---
...function_error_handle_middleware_chain.ts} | 96 ++++++++++---------
src/server.ts | 6 +-
..._function_error_handle_middleware_chain.ts | 77 +++++++++++++++
.../propagate_error_to_client_error_handle.ts | 73 --------------
4 files changed, 128 insertions(+), 124 deletions(-)
rename src/middleware/{propagate_error_to_client_error_handle.ts => inject_user_function_error_handle_middleware_chain.ts} (56%)
create mode 100644 test/middleware/inject_user_function_error_handle_middleware_chain.ts
delete mode 100644 test/middleware/propagate_error_to_client_error_handle.ts
diff --git a/src/middleware/propagate_error_to_client_error_handle.ts b/src/middleware/inject_user_function_error_handle_middleware_chain.ts
similarity index 56%
rename from src/middleware/propagate_error_to_client_error_handle.ts
rename to src/middleware/inject_user_function_error_handle_middleware_chain.ts
index a4cfc213c..84fd0299e 100644
--- a/src/middleware/propagate_error_to_client_error_handle.ts
+++ b/src/middleware/inject_user_function_error_handle_middleware_chain.ts
@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Request, Response, NextFunction, Express} from 'express';
+import {Express} from 'express';
import {HandlerFunction} from '../functions';
import {ILayer} from 'express-serve-static-core';
@@ -31,72 +31,74 @@ const COMMON_EXPRESS_OBJECT_PROPERTIES = [
/** The number of parameters on an express error handler. */
const EXPRESS_ERROR_HANDLE_PARAM_LENGTH = 4;
-/** A express app error handle. */
-interface ErrorHandle {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (err: Error, req: Request, res: Response, next: NextFunction): any;
-}
-
/**
- * Express middleware to propagate framework errors to the user function error handle.
- * This enables users to handle framework errors that would otherwise be handled by the
- * default Express error handle. If the user function doesn't have an error handle,
- * it falls back to the default Express error handle.
+ * Injects the user function error handle middleware and its subsequent middleware
+ * chain into the framework. This enables users to handle framework errors that would
+ * otherwise be handled by the default Express error handle.
+ * @param frameworkApp - Framework app
* @param userFunction - User handler function
*/
-export const createPropagateErrorToClientErrorHandleMiddleware = (
+export const injectUserFunctionErrorHandleMiddlewareChain = (
+ frameworkApp: Express,
userFunction: HandlerFunction
-): ErrorHandle => {
- const userFunctionErrorHandle =
- getFirstUserFunctionErrorHandleMiddleware(userFunction);
+) => {
+ // Check if user function is an express app that can register middleware.
+ if (!isExpressApp(userFunction)) {
+ return;
+ }
+
+ // Get the index of the user's first error handle middleware.
+ const firstErrorHandleMiddlewareIndex =
+ getFirstUserFunctionErrorHandleMiddlewareIndex(userFunction);
+ if (!firstErrorHandleMiddlewareIndex) {
+ return;
+ }
- return function (
- err: Error,
- req: Request,
- res: Response,
- next: NextFunction
+ // Inject their error handle middleware chain into the framework app.
+ const middlewares = (userFunction as Express)._router.stack;
+ for (
+ let index = firstErrorHandleMiddlewareIndex;
+ index < middlewares.length;
+ index++
) {
- // Propagate error to user function error handle.
- if (userFunctionErrorHandle) {
- return userFunctionErrorHandle(err, req, res, next);
+ const middleware = middlewares[index];
+
+ // We don't care about routes.
+ if (middleware.route) {
+ continue;
}
- // Propagate error to default Express error handle.
- return next();
- };
+ frameworkApp.use(middleware.handle);
+ }
};
/**
- * Returns the first user handler function defined error handle, if available.
- * @param userFunction - User handler function
+ * Returns if the user function contains common properties of an Express app.
+ * @param userFunction
*/
-const getFirstUserFunctionErrorHandleMiddleware = (
- userFunction: HandlerFunction
-): ErrorHandle | null => {
- if (!isExpressApp(userFunction)) {
- return null;
- }
+const isExpressApp = (userFunction: HandlerFunction): boolean => {
+ const userFunctionProperties = Object.getOwnPropertyNames(userFunction);
+ return COMMON_EXPRESS_OBJECT_PROPERTIES.every(prop =>
+ userFunctionProperties.includes(prop)
+ );
+};
+/**
+ * Returns the index of the first error handle middleware in the user function.
+ */
+const getFirstUserFunctionErrorHandleMiddlewareIndex = (
+ userFunction: HandlerFunction
+): number | null => {
const middlewares: ILayer[] = (userFunction as Express)._router.stack;
- for (const middleware of middlewares) {
+ for (let index = 0; index < middlewares.length; index++) {
+ const middleware = middlewares[index];
if (
middleware.handle &&
middleware.handle.length === EXPRESS_ERROR_HANDLE_PARAM_LENGTH
) {
- return middleware.handle as unknown as ErrorHandle;
+ return index;
}
}
return null;
};
-
-/**
- * Returns if the user function contains common properties of an Express app.
- * @param userFunction
- */
-const isExpressApp = (userFunction: HandlerFunction): boolean => {
- const userFunctionProperties = Object.getOwnPropertyNames(userFunction);
- return COMMON_EXPRESS_OBJECT_PROPERTIES.every(prop =>
- userFunctionProperties.includes(prop)
- );
-};
diff --git a/src/server.ts b/src/server.ts
index 87197afce..6734637b5 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -26,7 +26,7 @@ import {wrapUserFunction} from './function_wrappers';
import {asyncLocalStorageMiddleware} from './async_local_storage';
import {executionContextMiddleware} from './execution_context';
import {FrameworkOptions} from './options';
-import {createPropagateErrorToClientErrorHandleMiddleware} from './middleware/propagate_error_to_client_error_handle';
+import {injectUserFunctionErrorHandleMiddlewareChain} from './middleware/inject_user_function_error_handle_middleware_chain';
/**
* Creates and configures an Express application and returns an HTTP server
@@ -174,9 +174,7 @@ export function getServer(
}
if (options.propagateFrameworkErrors) {
- const errorHandleMiddleware =
- createPropagateErrorToClientErrorHandleMiddleware(userFunction);
- app.use(errorHandleMiddleware);
+ injectUserFunctionErrorHandleMiddlewareChain(app, userFunction);
}
return http.createServer(app);
diff --git a/test/middleware/inject_user_function_error_handle_middleware_chain.ts b/test/middleware/inject_user_function_error_handle_middleware_chain.ts
new file mode 100644
index 000000000..f565120f1
--- /dev/null
+++ b/test/middleware/inject_user_function_error_handle_middleware_chain.ts
@@ -0,0 +1,77 @@
+import * as assert from 'assert';
+import {NextFunction} from 'express';
+import {Request, Response} from '../../src';
+import * as express from 'express';
+import {injectUserFunctionErrorHandleMiddlewareChain} from '../../src/middleware/inject_user_function_error_handle_middleware_chain';
+import {ILayer} from 'express-serve-static-core';
+
+describe('injectUserFunctionErrorHandleMiddlewareChain', () => {
+ it('user app with error handle middleware injects into framework app', () => {
+ const frameworkApp = express();
+ const userApp = express();
+ userApp.use(appBErrorHandle);
+ function appBErrorHandle(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _err: Error,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _req: Request,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _res: Response,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _next: NextFunction
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ): any {}
+ userApp.use(appBFollowUpMiddleware);
+ function appBFollowUpMiddleware(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _req: Request,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _res: Response,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _next: NextFunction
+ ) {}
+
+ injectUserFunctionErrorHandleMiddlewareChain(frameworkApp, userApp);
+
+ const appAMiddleware = frameworkApp._router.stack;
+ const appAMiddlewareNames = appAMiddleware.map(
+ (middleware: ILayer) => middleware.name
+ );
+
+ assert.deepStrictEqual(appAMiddlewareNames, [
+ 'query',
+ 'expressInit',
+ 'appBErrorHandle',
+ 'appBFollowUpMiddleware',
+ ]);
+ });
+
+ it('user app without error handle not injected into framework app', () => {
+ const frameworkApp = express();
+ const userApp = express();
+ userApp.use(appBFollowUpMiddleware);
+ function appBFollowUpMiddleware(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _req: Request,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _res: Response,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _next: NextFunction
+ ) {}
+
+ injectUserFunctionErrorHandleMiddlewareChain(frameworkApp, userApp);
+
+ assert.strictEqual('_router' in frameworkApp, false);
+ });
+
+ it('non-express user app not injected into framework app', () => {
+ const frameworkApp = express();
+ const userApp = (_req: Request, res: Response) => {
+ res.send('Hello, World!');
+ };
+
+ injectUserFunctionErrorHandleMiddlewareChain(frameworkApp, userApp);
+
+ assert.strictEqual('_router' in frameworkApp, false);
+ });
+});
diff --git a/test/middleware/propagate_error_to_client_error_handle.ts b/test/middleware/propagate_error_to_client_error_handle.ts
deleted file mode 100644
index 3f6254d3d..000000000
--- a/test/middleware/propagate_error_to_client_error_handle.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import * as assert from 'assert';
-import * as sinon from 'sinon';
-import {NextFunction} from 'express';
-import {Request, Response} from '../../src';
-import {createPropagateErrorToClientErrorHandleMiddleware} from '../../src/middleware/propagate_error_to_client_error_handle';
-import * as express from 'express';
-
-describe('propagateErrorToClientErrorHandleMiddleware', () => {
- let request: Request;
- let response: Response;
- let next: NextFunction;
- let errListener = () => {};
- let error: Error;
- beforeEach(() => {
- error = Error('Something went wrong!');
- request = {
- setTimeout: sinon.spy(),
- on: sinon.spy(),
- } as unknown as Request;
- response = {
- on: sinon.spy(),
- } as unknown as Response;
- next = sinon.spy();
- errListener = sinon.spy();
- });
-
- it('user express app with error handle calls user function app error handle', () => {
- const app = express();
- app.use(
- (
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _err: Error,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _req: Request,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _res: Response,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _next: NextFunction
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ): any => {
- errListener();
- }
- );
-
- const middleware = createPropagateErrorToClientErrorHandleMiddleware(app);
- middleware(error, request, response, next);
-
- assert.strictEqual((errListener as sinon.SinonSpy).called, true);
- assert.strictEqual((next as sinon.SinonSpy).called, false);
- });
-
- it('user express app without error handle calls default express error handle', () => {
- const app = express();
-
- const middleware = createPropagateErrorToClientErrorHandleMiddleware(app);
- middleware(error, request, response, next);
-
- assert.strictEqual((errListener as sinon.SinonSpy).called, false);
- assert.strictEqual((next as sinon.SinonSpy).called, true);
- });
-
- it('non-express user app calls default express error handle', () => {
- const app = (_req: Request, res: Response) => {
- res.send('Hello, World!');
- };
-
- const middleware = createPropagateErrorToClientErrorHandleMiddleware(app);
- middleware(error, request, response, next);
-
- assert.strictEqual((errListener as sinon.SinonSpy).called, false);
- assert.strictEqual((next as sinon.SinonSpy).called, true);
- });
-});
From 9243eb026bb7bab7f1ecc73ade74c65d1e9bee4b Mon Sep 17 00:00:00 2001
From: Gregory Gaines <gainesagregory@gmail.com>
Date: Mon, 31 Mar 2025 12:38:56 -0400
Subject: [PATCH 8/8] test: Tighten test cases
---
..._function_error_handle_middleware_chain.ts | 15 ++-
..._function_error_handle_middleware_chain.ts | 97 +++++++++++--------
2 files changed, 63 insertions(+), 49 deletions(-)
diff --git a/src/middleware/inject_user_function_error_handle_middleware_chain.ts b/src/middleware/inject_user_function_error_handle_middleware_chain.ts
index 84fd0299e..fe3afd1b7 100644
--- a/src/middleware/inject_user_function_error_handle_middleware_chain.ts
+++ b/src/middleware/inject_user_function_error_handle_middleware_chain.ts
@@ -54,15 +54,12 @@ export const injectUserFunctionErrorHandleMiddlewareChain = (
return;
}
- // Inject their error handle middleware chain into the framework app.
- const middlewares = (userFunction as Express)._router.stack;
- for (
- let index = firstErrorHandleMiddlewareIndex;
- index < middlewares.length;
- index++
- ) {
- const middleware = middlewares[index];
-
+ // Inject middleware chain starting from the first error handle
+ // middleware into the framework app.
+ const middlewares = (userFunction as Express)._router.stack.slice(
+ firstErrorHandleMiddlewareIndex
+ );
+ for (const middleware of middlewares) {
// We don't care about routes.
if (middleware.route) {
continue;
diff --git a/test/middleware/inject_user_function_error_handle_middleware_chain.ts b/test/middleware/inject_user_function_error_handle_middleware_chain.ts
index f565120f1..944123e90 100644
--- a/test/middleware/inject_user_function_error_handle_middleware_chain.ts
+++ b/test/middleware/inject_user_function_error_handle_middleware_chain.ts
@@ -1,70 +1,87 @@
import * as assert from 'assert';
-import {NextFunction} from 'express';
+import {Express, NextFunction} from 'express';
import {Request, Response} from '../../src';
import * as express from 'express';
import {injectUserFunctionErrorHandleMiddlewareChain} from '../../src/middleware/inject_user_function_error_handle_middleware_chain';
import {ILayer} from 'express-serve-static-core';
describe('injectUserFunctionErrorHandleMiddlewareChain', () => {
- it('user app with error handle middleware injects into framework app', () => {
+ const userAppErrorHandleMiddleware = (
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ _err: Error,
+ _req: Request,
+ _res: Response,
+ _next: NextFunction
+ ) => {};
+ const userAppFollowUpErrorMiddleware = (
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ _err: Error,
+ _req: Request,
+ _res: Response,
+ _next: NextFunction
+ ) => {};
+ const userAppNormalMiddleware = (
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ _req: Request,
+ _res: Response,
+ _next: NextFunction
+ ) => {};
+
+ const getMiddlewareNames = (app: Express) => {
+ return app._router.stack.map((middleware: ILayer) => middleware.name);
+ };
+
+ it('user app with error handle middleware injects middleware chain into framework app', () => {
const frameworkApp = express();
const userApp = express();
- userApp.use(appBErrorHandle);
- function appBErrorHandle(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _err: Error,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _req: Request,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _res: Response,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _next: NextFunction
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- ): any {}
- userApp.use(appBFollowUpMiddleware);
- function appBFollowUpMiddleware(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _req: Request,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _res: Response,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _next: NextFunction
- ) {}
+ userApp.use(userAppErrorHandleMiddleware);
+ userApp.use(userAppNormalMiddleware);
+ userApp.use(userAppFollowUpErrorMiddleware);
injectUserFunctionErrorHandleMiddlewareChain(frameworkApp, userApp);
+ const frameworkAppMiddlewareNames = getMiddlewareNames(frameworkApp);
- const appAMiddleware = frameworkApp._router.stack;
- const appAMiddlewareNames = appAMiddleware.map(
- (middleware: ILayer) => middleware.name
+ assert.deepStrictEqual(frameworkAppMiddlewareNames, [
+ 'query',
+ 'expressInit',
+ 'userAppErrorHandleMiddleware',
+ 'userAppNormalMiddleware',
+ 'userAppFollowUpErrorMiddleware',
+ ]);
+ });
+
+ it('user app with error handle middleware ignores routes and injects middleware chain into framework app', () => {
+ const frameworkApp = express();
+ const userApp = express();
+ userApp.use(userAppErrorHandleMiddleware);
+ userApp.post(
+ '/foo',
+ (_req: Request, _res: Response, _next: NextFunction) => {}
);
+ userApp.use(userAppFollowUpErrorMiddleware);
+
+ injectUserFunctionErrorHandleMiddlewareChain(frameworkApp, userApp);
+ const frameworkAppMiddlewareNames = getMiddlewareNames(frameworkApp);
- assert.deepStrictEqual(appAMiddlewareNames, [
+ assert.deepStrictEqual(frameworkAppMiddlewareNames, [
'query',
'expressInit',
- 'appBErrorHandle',
- 'appBFollowUpMiddleware',
+ 'userAppErrorHandleMiddleware',
+ 'userAppFollowUpErrorMiddleware',
]);
});
- it('user app without error handle not injected into framework app', () => {
+ it('user app without error handle middleware chain not injected into framework app', () => {
const frameworkApp = express();
const userApp = express();
- userApp.use(appBFollowUpMiddleware);
- function appBFollowUpMiddleware(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _req: Request,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _res: Response,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- _next: NextFunction
- ) {}
+ userApp.use(userAppNormalMiddleware);
injectUserFunctionErrorHandleMiddlewareChain(frameworkApp, userApp);
assert.strictEqual('_router' in frameworkApp, false);
});
- it('non-express user app not injected into framework app', () => {
+ it('non-express user app middleware chain not injected into framework app', () => {
const frameworkApp = express();
const userApp = (_req: Request, res: Response) => {
res.send('Hello, World!');