Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/eventHandlers/InvocationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
import { isError } from '../utils/ensureErrorType';
import { nonNullProp } from '../utils/nonNull';
import { ReadOnlyError } from '../utils/ReadOnlyError';
import { WorkerChannel } from '../WorkerChannel';
import { EventHandler } from './EventHandler';
import RpcLogCategory = rpc.RpcLog.RpcLogCategory;
Expand Down Expand Up @@ -46,8 +47,18 @@ export class InvocationHandler extends EventHandler<'invocationRequest', 'invoca
let callback = channel.functionLoader.getCallback(functionId);

const preInvocContext: PreInvocationContext = {
hookData,
appHookData: channel.appHookData,
get hookData() {
return hookData;
},
set hookData(_obj) {
throw new ReadOnlyError('hookData');
},
get appHookData() {
return channel.appHookData;
},
set appHookData(_obj) {
throw new ReadOnlyError('appHookData');
},
invocationContext: context,
functionCallback: callback,
inputs,
Expand All @@ -64,8 +75,18 @@ export class InvocationHandler extends EventHandler<'invocationRequest', 'invoca
callback = preInvocContext.functionCallback;

const postInvocContext: PostInvocationContext = {
hookData,
appHookData: channel.appHookData,
get hookData() {
return hookData;
},
set hookData(_obj) {
throw new ReadOnlyError('hookData');
},
get appHookData() {
return channel.appHookData;
},
set appHookData(_obj) {
throw new ReadOnlyError('appHookData');
},
invocationContext: context,
inputs,
result: null,
Expand Down
15 changes: 13 additions & 2 deletions src/startApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { pathExists } from 'fs-extra';
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
import { loadScriptFile } from './loadScriptFile';
import { ensureErrorType } from './utils/ensureErrorType';
import { ReadOnlyError } from './utils/ReadOnlyError';
import { WorkerChannel } from './WorkerChannel';
import path = require('path');
import LogLevel = rpc.RpcLog.Level;
Expand All @@ -23,8 +24,18 @@ export async function startApp(functionAppDirectory: string, channel: WorkerChan
await channel.updatePackageJson(functionAppDirectory);
await loadEntryPointFile(functionAppDirectory, channel);
const appStartContext: AppStartContext = {
hookData: channel.appLevelOnlyHookData,
appHookData: channel.appHookData,
get hookData() {
return channel.appLevelOnlyHookData;
},
set hookData(_obj) {
throw new ReadOnlyError('hookData');
},
get appHookData() {
return channel.appHookData;
},
set appHookData(_obj) {
throw new ReadOnlyError('appHookData');
},
functionAppDirectory,
};
await channel.executeHooks('appStart', appStartContext);
Expand Down
9 changes: 9 additions & 0 deletions src/utils/ReadOnlyError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

export class ReadOnlyError extends TypeError {
isAzureFunctionsInternalException = true;
constructor(propertyName: string) {
super(`Cannot assign to read only property '${propertyName}'`);
}
}
54 changes: 54 additions & 0 deletions test/eventHandlers/InvocationHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,60 @@ describe('InvocationHandler', () => {
expect(hookData).to.equal('prepost');
});

it('enforces readonly property of hookData and appHookData in pre and post invocation hooks', async () => {
loader.getCallback.returns(async () => {});
loader.getRpcMetadata.returns(Binding.queue);

testDisposables.push(
coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => {
context.hookData['hello'] = 'world';
expect(() => {
// @ts-expect-error: setting readonly property
context.hookData = {
foo: 'bar',
};
}).to.throw(`Cannot assign to read only property 'hookData'`);
expect(() => {
// @ts-expect-error: setting readonly property
context.appHookData = {
foo: 'bar',
};
}).to.throw(`Cannot assign to read only property 'appHookData'`);
hookData += 'pre';
})
);

testDisposables.push(
coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => {
expect(context.hookData['hello']).to.equal('world');
expect(() => {
// @ts-expect-error: setting readonly property
context.hookData = {
foo: 'bar',
};
}).to.throw(`Cannot assign to read only property 'hookData'`);
expect(() => {
// @ts-expect-error: setting readonly property
context.appHookData = {
foo: 'bar',
};
}).to.throw(`Cannot assign to read only property 'appHookData'`);
hookData += 'post';
})
);

sendInvokeMessage([InputData.http]);
await stream.assertCalledWith(
Msg.receivedInvocLog(),
Msg.executingHooksLog(1, 'preInvocation'),
Msg.executedHooksLog('preInvocation'),
Msg.executingHooksLog(1, 'postInvocation'),
Msg.executedHooksLog('postInvocation'),
Msg.invocResponse([])
);
expect(hookData).to.equal('prepost');
});

it('appHookData changes from appStart hooks are persisted in invocation hook contexts', async () => {
const functionAppDirectory = __dirname;
const expectedAppHookData = {
Expand Down
30 changes: 30 additions & 0 deletions test/startApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,36 @@ describe('startApp', () => {
expect(hookData).to.equal('start1start2');
});

it('enforces readonly property of hookData and appHookData in appStart contexts', async () => {
const functionAppDirectory = __dirname;
testDisposables.push(
coreApi.registerHook('appStart', (context) => {
expect(() => {
// @ts-expect-error: setting readonly property
context.hookData = {
hello: 'world',
};
}).to.throw(`Cannot assign to read only property 'hookData'`);
expect(() => {
// @ts-expect-error: setting readonly property
context.appHookData = {
hello: 'world',
};
}).to.throw(`Cannot assign to read only property 'appHookData'`);
})
);

stream.addTestMessage(WorkerInitMsg.init(functionAppDirectory));

await stream.assertCalledWith(
WorkerInitMsg.receivedInitLog,
WorkerInitMsg.warning('Worker failed to load package.json: file does not exist'),
Msg.executingHooksLog(1, 'appStart'),
Msg.executedHooksLog('appStart'),
WorkerInitMsg.response
);
});

it('correctly sets hostVersion in core API', async () => {
const functionAppDirectory = __dirname;
const expectedHostVersion = '2.7.0';
Expand Down
6 changes: 4 additions & 2 deletions types-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ declare module '@azure/functions-core' {
interface HookContext {
/**
* The recommended place to share data between hooks in the same scope (app-level vs invocation-level)
* This object is readonly and attempting to overwrite it will throw an error
*/
hookData: HookData;
readonly hookData: HookData;
/**
* The recommended place to share data across scopes for all hooks
* This object is readonly and attempting to overwrite it will throw an error
*/
appHookData: HookData;
readonly appHookData: HookData;
}

/**
Expand Down