Skip to content

Commit

Permalink
fix: freeze lambda context object (silvermine#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
yokuze committed Mar 7, 2019
1 parent 250c469 commit b2c7dac
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 10 deletions.
34 changes: 30 additions & 4 deletions src/Application.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Callback } from 'aws-lambda';
import { Callback, Context } from 'aws-lambda';
import Router from './Router';
import { RequestEvent, HandlerContext } from './request-response-types';
import { StringUnknownMap } from './utils/common-types';
import { StringUnknownMap, Writable } from './utils/common-types';
import { Request, Response } from '.';
import _ from 'underscore';

export default class Application extends Router {

Expand Down Expand Up @@ -85,8 +86,8 @@ export default class Application extends Router {
* @param context The context provided to the Lambda handler
* @param cb The callback provided to the Lambda handler
*/
public run(evt: RequestEvent, context: HandlerContext, cb: Callback): void {
const req = new Request(this, evt, context),
public run(evt: RequestEvent, context: Context, cb: Callback): void {
const req = new Request(this, evt, this._createHandlerContext(context)),
resp = new Response(this, req, cb);

this.handle(undefined, req, resp, (err: unknown): void => {
Expand All @@ -99,4 +100,29 @@ export default class Application extends Router {
});
}

private _createHandlerContext(context: Context): HandlerContext {
// keys should exist on both `HandlerContext` and `Context`
const keys: (keyof HandlerContext & keyof Context)[] = [
'functionName', 'functionVersion', 'invokedFunctionArn', 'memoryLimitInMB',
'awsRequestId', 'logGroupName', 'logStreamName', 'identity', 'clientContext',
'getRemainingTimeInMillis',
];

let handlerContext: Writable<HandlerContext>;

handlerContext = _.reduce(keys, (memo, key) => {
let contextValue = context[key];

if (typeof contextValue === 'object' && contextValue) {
// Freeze sub-objects
memo[key] = Object.freeze(_.extend({}, contextValue));
} else if (typeof contextValue !== 'undefined') {
memo[key] = contextValue;
}
return memo;
}, {} as Writable<HandlerContext>);

return Object.freeze(handlerContext);
}

}
2 changes: 0 additions & 2 deletions src/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,6 @@ export default class Request {

this.eventSourceType = ('elb' in event.requestContext) ? Request.SOURCE_ALB : Request.SOURCE_APIGW;

// TODO: should something be done to limit what's exposed by these contexts? For
// example, make properties read-only and don't expose the callback function, etc.
this.context = context;
this.requestContext = event.requestContext;

Expand Down
13 changes: 12 additions & 1 deletion src/request-response-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,18 @@ export interface ResponseResult extends APIGatewayProxyResult {
* The `context` object passed to a Lambda handler.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface HandlerContext extends Context {}
export interface HandlerContext extends Readonly<Pick<Context,
'functionName'
| 'functionVersion'
| 'invokedFunctionArn'
| 'memoryLimitInMB'
| 'awsRequestId'
| 'logGroupName'
| 'logStreamName'
| 'identity'
| 'clientContext'
| 'getRemainingTimeInMillis'
>> {}


/* API GATEWAY TYPES (we export these with our own names to make it easier to modify them
Expand Down
2 changes: 2 additions & 0 deletions src/utils/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface StringMap { [s: string]: string }
export interface StringUnknownMap { [s: string]: unknown }
export interface StringArrayOfStringsMap { [s: string]: string[] }
export interface KeyValueStringObject { [k: string]: (string | string[] | KeyValueStringObject) }
// Removes `readonly` modifier and make all keys writable again
export type Writable<T> = { -readonly [P in keyof T]-?: T[P] };

export function isStringMap(o: any): o is StringMap {
if (!_.isObject(o) || _.isArray(o)) { // because arrays are objects
Expand Down
45 changes: 45 additions & 0 deletions tests/integration-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,4 +547,49 @@ describe('integration tests', () => {

});

describe('request object', () => {

it('has an immutable context property', () => {
let evt = makeRequestEvent('/test', apiGatewayRequest(), 'GET'),
ctx = handlerContext(true),
handler;

function isPropFrozen(obj: any, key: string): boolean {
try {
obj[key] = 'change';
return false;
} catch(e) {
if (e instanceof Error) {
return e.message.indexOf('Cannot assign to read only property') !== -1;
}
return false;
}
}

handler = spy((req: Request, resp: Response) => {
expect(req.context).to.be.an('object');

expect(isPropFrozen(req.context, 'awsRequestId'));
expect(isPropFrozen(req.context, 'clientContext'));

if (req.context.clientContext) {
expect(isPropFrozen(req.context.clientContext, 'clientContext'));
}

if (req.context.identity) {
expect(isPropFrozen(req.context.identity, 'cognitoIdentityId'));
}

resp.send('test');
});
app.get('*', handler);

app.run(evt, ctx, spy());

// Make sure the handler ran, otherwise the test is invalid.
assert.calledOnce(handler);
});

});

});
34 changes: 31 additions & 3 deletions tests/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import {
APIGatewayEventRequestContext,
ApplicationLoadBalancerEventRequestContext,
APIGatewayRequestEvent,
HandlerContext,
ApplicationLoadBalancerRequestEvent } from '../src/request-response-types';
import { Context } from 'aws-lambda';

export const handlerContext = (): HandlerContext => {
return {
export const handlerContext = (fillAllFields: boolean = false): Context => {
let ctx: Context;

ctx = {
callbackWaitsForEmptyEventLoop: true,
logGroupName: '/aws/lambda/echo-api-prd-echo',
logStreamName: '2019/01/31/[$LATEST]bb001267fb004ffa8e1710bba30b4ae7',
Expand All @@ -21,6 +23,32 @@ export const handlerContext = (): HandlerContext => {
fail: () => undefined,
succeed: () => undefined,
};

if (fillAllFields) {
ctx.identity = {
cognitoIdentityId: 'cognitoIdentityId',
cognitoIdentityPoolId: 'cognitoIdentityPoolId',
};

ctx.clientContext = {
client: {
installationId: 'installationId',
appTitle: 'appTitle',
appVersionName: 'appVersionName',
appVersionCode: 'appVersionCode',
appPackageName: 'appPackageName',
},
env: {
platformVersion: 'platformVersion',
platform: 'platform',
make: 'make',
model: 'model',
locale: 'locale',
},
};
}

return ctx;
};

export const apiGatewayRequestContext = (): APIGatewayEventRequestContext => {
Expand Down

0 comments on commit b2c7dac

Please sign in to comment.