diff --git a/src/eventHandlers/InvocationHandler.ts b/src/eventHandlers/InvocationHandler.ts index f9b22e3e..f22c851d 100644 --- a/src/eventHandlers/InvocationHandler.ts +++ b/src/eventHandlers/InvocationHandler.ts @@ -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; @@ -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, @@ -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, diff --git a/src/startApp.ts b/src/startApp.ts index 0f9a061b..38ca12e4 100644 --- a/src/startApp.ts +++ b/src/startApp.ts @@ -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; @@ -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); diff --git a/src/utils/ReadOnlyError.ts b/src/utils/ReadOnlyError.ts new file mode 100644 index 00000000..ecacc6b0 --- /dev/null +++ b/src/utils/ReadOnlyError.ts @@ -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}'`); + } +} diff --git a/test/eventHandlers/InvocationHandler.test.ts b/test/eventHandlers/InvocationHandler.test.ts index f5addbc4..f82d2065 100644 --- a/test/eventHandlers/InvocationHandler.test.ts +++ b/test/eventHandlers/InvocationHandler.test.ts @@ -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 = { diff --git a/test/startApp.test.ts b/test/startApp.test.ts index abfa3da4..1352564b 100644 --- a/test/startApp.test.ts +++ b/test/startApp.test.ts @@ -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'; diff --git a/types-core/index.d.ts b/types-core/index.d.ts index dd1a2808..fab9f637 100644 --- a/types-core/index.d.ts +++ b/types-core/index.d.ts @@ -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; } /**