Skip to content

Commit 919dc4c

Browse files
refactor!: move function wrapping earlier
This commit extracts the logic for wrapping user functions into its own module and adds unit tests. It also refactors the code path that wraps the user function earlier in the initialization logic. This removes registration from the critical request path.
1 parent 5856009 commit 919dc4c

File tree

5 files changed

+368
-230
lines changed

5 files changed

+368
-230
lines changed

src/invoker.ts

Lines changed: 7 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,10 @@
1919
// with non-HTTP trigger).
2020
// - ANY (all methods) '/*' for executing functions (only for servers handling
2121
// functions with HTTP trigger).
22-
23-
// eslint-disable-next-line node/no-deprecated-api
24-
import * as domain from 'domain';
2522
import * as express from 'express';
2623
import * as http from 'http';
2724
import {FUNCTION_STATUS_HEADER_FIELD} from './types';
2825
import {sendCrashResponse} from './logger';
29-
import {isBinaryCloudEvent, getBinaryCloudEventContext} from './cloudevents';
30-
import {
31-
HttpFunction,
32-
EventFunction,
33-
EventFunctionWithCallback,
34-
CloudEventFunction,
35-
CloudEventFunctionWithCallback,
36-
} from './functions';
3726

3827
// We optionally annotate the express Request with a rawBody field.
3928
// Express leaves the Express namespace open to allow merging of new fields.
@@ -61,8 +50,13 @@ export const setLatestRes = (res: express.Response) => {
6150
* @param err Error from function execution.
6251
* @param res Express response object.
6352
*/
64-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
65-
function sendResponse(result: any, err: Error | null, res: express.Response) {
53+
54+
export function sendResponse(
55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
result: any,
57+
err: Error | null,
58+
res: express.Response
59+
) {
6660
if (err) {
6761
res.set(FUNCTION_STATUS_HEADER_FIELD, 'error');
6862
// Sending error message back is fine for Pub/Sub-based functions as they do
@@ -92,144 +86,6 @@ function sendResponse(result: any, err: Error | null, res: express.Response) {
9286
}
9387
}
9488

95-
/**
96-
* Wraps the provided function into an Express handler function with additional
97-
* instrumentation logic.
98-
* @param execute Runs user's function.
99-
* @return An Express handler function.
100-
*/
101-
export function makeHttpHandler(execute: HttpFunction): express.RequestHandler {
102-
return (req: express.Request, res: express.Response) => {
103-
const d = domain.create();
104-
// Catch unhandled errors originating from this request.
105-
d.on('error', err => {
106-
if (res.locals.functionExecutionFinished) {
107-
console.error(`Exception from a finished function: ${err}`);
108-
} else {
109-
res.locals.functionExecutionFinished = true;
110-
sendCrashResponse({err, res});
111-
}
112-
});
113-
d.run(() => {
114-
process.nextTick(() => {
115-
execute(req, res);
116-
});
117-
});
118-
};
119-
}
120-
121-
/**
122-
* Wraps cloudevent function (or cloudevent function with callback) in HTTP
123-
* function signature.
124-
* @param userFunction User's function.
125-
* @return HTTP function which wraps the provided event function.
126-
*/
127-
export function wrapCloudEventFunction(
128-
userFunction: CloudEventFunction | CloudEventFunctionWithCallback
129-
): HttpFunction {
130-
return (req: express.Request, res: express.Response) => {
131-
const callback = process.domain.bind(
132-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133-
(err: Error | null, result: any) => {
134-
if (res.locals.functionExecutionFinished) {
135-
console.log('Ignoring extra callback call');
136-
} else {
137-
res.locals.functionExecutionFinished = true;
138-
if (err) {
139-
console.error(err.stack);
140-
}
141-
sendResponse(result, err, res);
142-
}
143-
}
144-
);
145-
let cloudevent = req.body;
146-
if (isBinaryCloudEvent(req)) {
147-
cloudevent = getBinaryCloudEventContext(req);
148-
cloudevent.data = req.body;
149-
}
150-
// Callback style if user function has more than 1 argument.
151-
if (userFunction!.length > 1) {
152-
const fn = userFunction as CloudEventFunctionWithCallback;
153-
return fn(cloudevent, callback);
154-
}
155-
156-
const fn = userFunction as CloudEventFunction;
157-
Promise.resolve()
158-
.then(() => {
159-
const result = fn(cloudevent);
160-
return result;
161-
})
162-
.then(
163-
result => {
164-
callback(null, result);
165-
},
166-
err => {
167-
callback(err, undefined);
168-
}
169-
);
170-
};
171-
}
172-
173-
/**
174-
* Wraps event function (or event function with callback) in HTTP function
175-
* signature.
176-
* @param userFunction User's function.
177-
* @return HTTP function which wraps the provided event function.
178-
*/
179-
export function wrapEventFunction(
180-
userFunction: EventFunction | EventFunctionWithCallback
181-
): HttpFunction {
182-
return (req: express.Request, res: express.Response) => {
183-
const event = req.body;
184-
const callback = process.domain.bind(
185-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
186-
(err: Error | null, result: any) => {
187-
if (res.locals.functionExecutionFinished) {
188-
console.log('Ignoring extra callback call');
189-
} else {
190-
res.locals.functionExecutionFinished = true;
191-
if (err) {
192-
console.error(err.stack);
193-
}
194-
sendResponse(result, err, res);
195-
}
196-
}
197-
);
198-
const data = event.data;
199-
let context = event.context;
200-
if (context === undefined) {
201-
// Support legacy events and CloudEvents in structured content mode, with
202-
// context properties represented as event top-level properties.
203-
// Context is everything but data.
204-
context = event;
205-
// Clear the property before removing field so the data object
206-
// is not deleted.
207-
context.data = undefined;
208-
delete context.data;
209-
}
210-
// Callback style if user function has more than 2 arguments.
211-
if (userFunction!.length > 2) {
212-
const fn = userFunction as EventFunctionWithCallback;
213-
return fn(data, context, callback);
214-
}
215-
216-
const fn = userFunction as EventFunction;
217-
Promise.resolve()
218-
.then(() => {
219-
const result = fn(data, context);
220-
return result;
221-
})
222-
.then(
223-
result => {
224-
callback(null, result);
225-
},
226-
err => {
227-
callback(err, undefined);
228-
}
229-
);
230-
};
231-
}
232-
23389
// Use an exit code which is unused by Node.js:
23490
// https://nodejs.org/api/process.html#process_exit_codes
23591
const killInstance = process.exit.bind(process, 16);

src/router.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.

src/server.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
import * as bodyParser from 'body-parser';
1616
import * as express from 'express';
1717
import * as http from 'http';
18+
import * as onFinished from 'on-finished';
1819
import {HandlerFunction} from './functions';
1920
import {SignatureType} from './types';
2021
import {setLatestRes} from './invoker';
21-
import {registerFunctionRoutes} from './router';
2222
import {legacyPubSubEventMiddleware} from './pubsub_middleware';
2323
import {cloudeventToBackgroundEventMiddleware} from './middleware/cloudevent_to_background_event';
2424
import {backgroundEventToCloudEventMiddleware} from './middleware/background_event_to_cloudevent';
25+
import {wrapUserFunction} from './wrappers';
2526

2627
/**
2728
* Creates and configures an Express application and returns an HTTP server
@@ -117,6 +118,28 @@ export function getServer(
117118
app.use(backgroundEventToCloudEventMiddleware);
118119
}
119120

120-
registerFunctionRoutes(app, userFunction, functionSignatureType);
121+
if (functionSignatureType === 'http') {
122+
app.use('/favicon.ico|/robots.txt', (req, res) => {
123+
// Neither crawlers nor browsers attempting to pull the icon find the body
124+
// contents particularly useful, so we send nothing in the response body.
125+
res.status(404).send(null);
126+
});
127+
128+
app.use('/*', (req, res, next) => {
129+
onFinished(res, (err, res) => {
130+
res.locals.functionExecutionFinished = true;
131+
});
132+
next();
133+
});
134+
}
135+
136+
// Set up the routes for the user's function
137+
const requestHandler = wrapUserFunction(userFunction, functionSignatureType);
138+
if (functionSignatureType === 'http') {
139+
app.all('/*', requestHandler);
140+
} else {
141+
app.post('/*', requestHandler);
142+
}
143+
121144
return http.createServer(app);
122145
}

0 commit comments

Comments
 (0)