Skip to content

Commit 48c7ce4

Browse files
joeyzhao2018claudepurple4reinaBridgeAR
authored
fix(lambda): handle missing context for some lambda functions (#7445)
* fix(lambda): handle missing context for Lambda Authorizers Lambda Authorizers and some other Lambda handler types do not receive a context object - they only receive an event parameter. Previously, the extractContext() function would throw "Could not extract context" when it couldn't find a context object with getRemainingTimeInMillis. This change makes extractContext() return undefined instead of throwing, and updates the datadog() wrapper to skip timeout checking when context is not available. This allows Lambda Authorizers to be instrumented without crashing. Fixes: DataDog/datadog-lambda-js#721 Co-Authored-By: Claude Opus 4.5 <[email protected]> * Undefined cleanup. * Add getRemainingTimeInMillis check back. * Update packages/dd-trace/src/lambda/handler.js Co-authored-by: Ruben Bridgewater <[email protected]> * Move extractContext to separate file and add more tesets. * Linting. --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Rey Abolofia <[email protected]> Co-authored-by: Rey Abolofia <[email protected]> Co-authored-by: Ruben Bridgewater <[email protected]>
1 parent 6bef168 commit 48c7ce4

File tree

5 files changed

+248
-18
lines changed

5 files changed

+248
-18
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict'
2+
3+
const log = require('../log')
4+
5+
/**
6+
* Extracts the context from the given Lambda handler arguments.
7+
*
8+
* It is possible for users to define a lambda function without specifying a
9+
* context arg. In these cases, this function returns null instead of throwing
10+
* an error.
11+
*
12+
* @param {unknown[]} args any amount of arguments
13+
* @returns {object | null}
14+
*/
15+
exports.extractContext = function extractContext (args) {
16+
let context = null
17+
for (let i = 0; i < args.length && i < 3; i++) {
18+
if (args[i] && typeof args[i].getRemainingTimeInMillis === 'function') {
19+
context = args[i]
20+
break
21+
}
22+
}
23+
if (!context) {
24+
log.debug('Unable to extract context object from Lambda handler arguments')
25+
}
26+
return context
27+
}

packages/dd-trace/src/lambda/handler.js

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { channel } = require('../../../datadog-instrumentations/src/helpers/instr
55
const { ERROR_MESSAGE, ERROR_TYPE } = require('../constants')
66
const { getValueFromEnvSources } = require('../config/helper')
77
const { ImpendingTimeout } = require('./runtime/errors')
8+
const { extractContext } = require('./context')
89

910
const globalTracer = global._ddtrace
1011
const tracer = globalTracer._tracer
@@ -60,23 +61,6 @@ function crashFlush () {
6061
}
6162
}
6263

63-
/**
64-
* Extracts the context from the given Lambda handler arguments.
65-
*
66-
* @param {unknown[]} args any amount of arguments
67-
* @returns the context, if extraction was succesful.
68-
*/
69-
function extractContext (args) {
70-
let context = args.length > 1 ? args[1] : undefined
71-
if (context === undefined || context.getRemainingTimeInMillis === undefined) {
72-
context = args.length > 2 ? args[2] : undefined
73-
if (context === undefined || context.getRemainingTimeInMillis === undefined) {
74-
throw new Error('Could not extract context')
75-
}
76-
}
77-
return context
78-
}
79-
8064
/**
8165
* Patches your AWS Lambda handler function to add some tracing support.
8266
*
@@ -86,7 +70,10 @@ exports.datadog = function datadog (lambdaHandler) {
8670
return (...args) => {
8771
const context = extractContext(args)
8872

89-
checkTimeout(context)
73+
if (context) {
74+
checkTimeout(context)
75+
}
76+
9077
const result = lambdaHandler.apply(this, args)
9178
if (result && typeof result.then === 'function') {
9279
return result.then((res) => {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
const { describe, it } = require('mocha')
5+
const { extractContext } = require('../../src/lambda/context')
6+
7+
describe('context', () => {
8+
describe('extractContext', () => {
9+
const assertExtractContext = (args, doesExtract) => {
10+
it(`properly extracts context object from args length ${args.length}`, () => {
11+
const ctx = extractContext(args)
12+
if (doesExtract) {
13+
assert.strictEqual(typeof ctx.getRemainingTimeInMillis, 'function')
14+
assert.strictEqual(ctx.getRemainingTimeInMillis(), 100)
15+
} else {
16+
assert.strictEqual(ctx, null)
17+
}
18+
})
19+
}
20+
21+
const contexts = [
22+
[null, false],
23+
[[], false],
24+
[{}, false],
25+
[{ getRemainingTimeInMillis: null }, false],
26+
[{ getRemainingTimeInMillis: undefined }, false],
27+
[{ getRemainingTimeInMillis: 'not a function' }, false],
28+
[{ getRemainingTimeInMillis: () => 100 }, true],
29+
]
30+
31+
assertExtractContext([], false)
32+
assertExtractContext([{}], false)
33+
contexts.forEach(([context, doesExtract], index) => {
34+
describe(`using context case ${index + 1}`, () => {
35+
assertExtractContext([{}, context], doesExtract)
36+
assertExtractContext([{}, {}, context], doesExtract)
37+
assertExtractContext([{}, {}, {}, context], false)
38+
})
39+
})
40+
})
41+
})

packages/dd-trace/test/lambda/fixtures/handler.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,67 @@ const errorHandler = async (_event, _context) => {
6565
throw new CustomError('my error')
6666
}
6767

68+
/**
69+
* Lambda Authorizer handler - only receives event, no context.
70+
* This is the signature used by API Gateway Lambda Authorizers.
71+
*/
72+
const authorizerHandler = async (event) => {
73+
// Simulate a simple authorizer that returns an IAM policy
74+
return {
75+
principalId: 'user123',
76+
policyDocument: {
77+
Version: '2012-10-17',
78+
Statement: [
79+
{
80+
Action: 'execute-api:Invoke',
81+
Effect: 'Allow',
82+
Resource: event.methodArn || '*',
83+
},
84+
],
85+
},
86+
}
87+
}
88+
89+
/**
90+
* Synchronous Lambda Authorizer handler - only receives event, no context.
91+
*/
92+
const authorizerHandlerSync = (event) => {
93+
return {
94+
principalId: 'user123',
95+
policyDocument: {
96+
Version: '2012-10-17',
97+
Statement: [
98+
{
99+
Action: 'execute-api:Invoke',
100+
Effect: 'Allow',
101+
Resource: event.methodArn || '*',
102+
},
103+
],
104+
},
105+
}
106+
}
107+
108+
/**
109+
* Lambda Authorizer handler that throws an error.
110+
*/
111+
const authorizerErrorHandler = async (event) => {
112+
class AuthorizationError extends Error {
113+
constructor (message) {
114+
super(message)
115+
Object.defineProperty(this, 'name', { value: 'AuthorizationError' })
116+
}
117+
}
118+
throw new AuthorizationError('Unauthorized')
119+
}
120+
68121
module.exports = {
69122
finishSpansEarlyTimeoutHandler,
70123
handler,
71124
swappedArgsHandler,
72125
timeoutHandler,
73126
errorHandler,
74127
callbackHandler,
128+
authorizerHandler,
129+
authorizerHandlerSync,
130+
authorizerErrorHandler,
75131
}

packages/dd-trace/test/lambda/index.spec.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,125 @@ describe('lambda', () => {
218218
})
219219
})
220220

221+
describe('lambda authorizers (no context)', () => {
222+
beforeEach(setup)
223+
224+
afterEach(() => {
225+
restoreEnv()
226+
return closeAgent()
227+
})
228+
229+
it('patches async lambda authorizer correctly (event only, no context)', async () => {
230+
// Set the desired handler to patch
231+
process.env.DD_LAMBDA_HANDLER = 'handler.authorizerHandler'
232+
// Load the agent and re-register hook for patching.
233+
await loadAgent()
234+
235+
// Lambda Authorizers only receive an event, no context
236+
const _event = {
237+
type: 'REQUEST',
238+
methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/stage/GET/resource',
239+
headers: {
240+
Authorization: 'Bearer token123',
241+
},
242+
}
243+
244+
// Mock `datadog-lambda` handler resolve and import.
245+
const _handlerPath = path.resolve(__dirname, './fixtures/handler.js')
246+
const app = require(_handlerPath)
247+
datadog = require('./fixtures/datadog-lambda')
248+
249+
// Run the function without context - this should NOT throw
250+
const result = await datadog(app.authorizerHandler)(_event)
251+
252+
assert.notStrictEqual(result, undefined)
253+
assert.strictEqual(result.principalId, 'user123')
254+
assert.strictEqual(result.policyDocument.Statement[0].Effect, 'Allow')
255+
256+
// Expect traces to be correct.
257+
const checkTraces = agent.assertSomeTraces((_traces) => {
258+
const traces = _traces[0]
259+
assert.strictEqual(traces.length, 1)
260+
traces.forEach((trace) => {
261+
assert.strictEqual(trace.error, 0)
262+
})
263+
})
264+
await checkTraces
265+
})
266+
267+
it('patches sync lambda authorizer correctly (event only, no context)', async () => {
268+
// Set the desired handler to patch
269+
process.env.DD_LAMBDA_HANDLER = 'handler.authorizerHandlerSync'
270+
// Load the agent and re-register hook for patching.
271+
await loadAgent()
272+
273+
// Lambda Authorizers only receive an event, no context
274+
const _event = {
275+
type: 'REQUEST',
276+
methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/stage/GET/resource',
277+
}
278+
279+
// Mock `datadog-lambda` handler resolve and import.
280+
const _handlerPath = path.resolve(__dirname, './fixtures/handler.js')
281+
const app = require(_handlerPath)
282+
datadog = require('./fixtures/datadog-lambda')
283+
284+
// Run the function without context - this should NOT throw
285+
// Note: datadog-lambda mock wraps in async, so we need to await
286+
const result = await datadog(app.authorizerHandlerSync)(_event)
287+
288+
assert.notStrictEqual(result, undefined)
289+
assert.strictEqual(result.principalId, 'user123')
290+
assert.strictEqual(result.policyDocument.Statement[0].Effect, 'Allow')
291+
292+
// Expect traces to be correct.
293+
const checkTraces = agent.assertSomeTraces((_traces) => {
294+
const traces = _traces[0]
295+
assert.strictEqual(traces.length, 1)
296+
traces.forEach((trace) => {
297+
assert.strictEqual(trace.error, 0)
298+
})
299+
})
300+
await checkTraces
301+
})
302+
303+
it('handles errors in lambda authorizer correctly (event only, no context)', async () => {
304+
// Set the desired handler to patch
305+
process.env.DD_LAMBDA_HANDLER = 'handler.authorizerErrorHandler'
306+
// Load the agent and re-register hook for patching.
307+
await loadAgent()
308+
309+
const _event = {
310+
type: 'REQUEST',
311+
methodArn: 'arn:aws:execute-api:us-east-1:123456789012:api-id/stage/GET/resource',
312+
}
313+
314+
// Mock `datadog-lambda` handler resolve and import.
315+
const _handlerPath = path.resolve(__dirname, './fixtures/handler.js')
316+
const app = require(_handlerPath)
317+
datadog = require('./fixtures/datadog-lambda')
318+
319+
// Run the function - should throw but not because of missing context
320+
try {
321+
await datadog(app.authorizerErrorHandler)(_event)
322+
assert.fail('Expected error to be thrown')
323+
} catch (e) {
324+
assert.strictEqual(e.name, 'AuthorizationError')
325+
assert.strictEqual(e.message, 'Unauthorized')
326+
}
327+
328+
// Expect traces to be correct with error.
329+
const checkTraces = agent.assertSomeTraces((_traces) => {
330+
const traces = _traces[0]
331+
assert.strictEqual(traces.length, 1)
332+
traces.forEach((trace) => {
333+
assert.strictEqual(trace.error, 1)
334+
})
335+
})
336+
await checkTraces
337+
})
338+
})
339+
221340
describe('timeout spans', () => {
222341
beforeEach(setup)
223342

0 commit comments

Comments
 (0)