Skip to content

Commit 2463ff6

Browse files
committed
fix(logger): keep hostile child metadata from throwing into the caller
1 parent 356f2b2 commit 2463ff6

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

packages/logger/src/index.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,5 +254,46 @@ describe('Logger', () => {
254254
expect(parsed.module).toBe('Test')
255255
expect(parsed.serializationError).toBe(true)
256256
})
257+
258+
test('should not throw when withMetadata receives a throwing getter', () => {
259+
const hostile = {
260+
safe: 'kept',
261+
get boom() {
262+
throw new Error('getter exploded')
263+
},
264+
} as unknown as Parameters<Logger['withMetadata']>[0]
265+
266+
let child: Logger | undefined
267+
expect(() => {
268+
child = createEnabledLogger().withMetadata(hostile)
269+
}).not.toThrow()
270+
271+
expect(() => child?.info('hello')).not.toThrow()
272+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
273+
expect(parsed.message).toBe('hello')
274+
expect(parsed.safe).toBe('kept')
275+
expect(parsed.boom).toBe('[Unreadable]')
276+
})
277+
278+
test('should not throw when withMetadata receives a hostile proxy', () => {
279+
const hostile = new Proxy(
280+
{},
281+
{
282+
ownKeys() {
283+
throw new Error('ownKeys exploded')
284+
},
285+
}
286+
) as Parameters<Logger['withMetadata']>[0]
287+
288+
let child: Logger | undefined
289+
expect(() => {
290+
child = createEnabledLogger().withMetadata(hostile)
291+
}).not.toThrow()
292+
293+
expect(() => child?.info('hello')).not.toThrow()
294+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
295+
expect(parsed.message).toBe('hello')
296+
expect(parsed.metadataError).toBe(true)
297+
})
257298
})
258299
})

packages/logger/src/index.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,34 @@ const serializeEntry = (base: Record<string, unknown>, args: unknown[]): string
188188
return JSON.stringify({ ...base, serializationError: true }, tolerantReplacer())
189189
}
190190

191+
/**
192+
* Copies caller-supplied metadata into a plain object without ever throwing.
193+
*
194+
* `LoggerMetadata` is structurally typed, so nothing stops a caller from handing
195+
* over an object carrying a throwing getter or a hostile proxy. A spread invokes
196+
* those traps, so the copy degrades key-by-key and finally to a marker rather
197+
* than raising inside the caller's code path.
198+
*/
199+
const materializeMetadata = (metadata: LoggerMetadata): LoggerMetadata => {
200+
try {
201+
return { ...metadata }
202+
} catch {}
203+
204+
const safe: LoggerMetadata = {}
205+
try {
206+
for (const key of Object.keys(metadata)) {
207+
try {
208+
safe[key] = metadata[key]
209+
} catch {
210+
safe[key] = '[Unreadable]'
211+
}
212+
}
213+
return safe
214+
} catch {}
215+
216+
return { metadataError: true }
217+
}
218+
191219
/**
192220
* Logger class for standardized console logging
193221
*
@@ -240,7 +268,7 @@ export class Logger {
240268
child.module = this.module
241269
child.config = this.config
242270
child.isDev = this.isDev
243-
child.metadata = { ...this.metadata, ...metadata }
271+
child.metadata = { ...this.metadata, ...materializeMetadata(metadata) }
244272
return child
245273
}
246274

0 commit comments

Comments
 (0)