From bd1691be945284f5870d71b4ba3e257c07a09af5 Mon Sep 17 00:00:00 2001 From: John Lwin Date: Tue, 21 Jul 2026 01:08:20 -0700 Subject: [PATCH 1/3] feat(node-http-handler): pass client logger to request handlers --- .../pass-client-logger-to-request-handler.md | 17 ++++ .../httpExtensionConfiguration.spec.ts | 82 +++++++++++++++++++ .../extensions/httpExtensionConfiguration.ts | 21 +++++ .../src/node-http-handler.spec.ts | 53 ++++++++++++ .../src/node-http-handler.ts | 3 + .../src/undici-http-handler.spec.ts | 13 ++- .../src/undici-http-handler.ts | 6 +- 7 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 .changeset/pass-client-logger-to-request-handler.md create mode 100644 packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts diff --git a/.changeset/pass-client-logger-to-request-handler.md b/.changeset/pass-client-logger-to-request-handler.md new file mode 100644 index 00000000000..48316e49673 --- /dev/null +++ b/.changeset/pass-client-logger-to-request-handler.md @@ -0,0 +1,17 @@ +--- +"@smithy/core": minor +"@smithy/node-http-handler": minor +"@smithy/undici-http-handler": minor +--- + +feat: pass client logger to request handlers + +Injects the client-level logger into the request handler during +`getHttpHandlerExtensionConfiguration`, so handler-emitted diagnostics +(e.g. socket exhaustion warnings) route through the customer's logger +instead of `console.warn`. + +Skips injection when the logger is `NoOpLogger` (the default), +preserving the existing console fallback for customers who never set a logger. +Handler-level loggers take precedence via nullish assignment in +`updateHttpClientConfig`. diff --git a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts new file mode 100644 index 00000000000..0eee09fcde9 --- /dev/null +++ b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getHttpHandlerExtensionConfiguration } from "./httpExtensionConfiguration"; + +describe("getHttpHandlerExtensionConfiguration", () => { + const createMockHandler = () => ({ + metadata: { handlerProtocol: "http/1.1" }, + handle: vi.fn(), + updateHttpClientConfig: vi.fn(), + httpHandlerConfigs: vi.fn().mockReturnValue({}), + }); + + const createMockLogger = () => ({ + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }); + + describe("client logger injection", () => { + it("injects logger into requestHandler when logger is explicitly set", () => { + const handler = createMockHandler(); + const logger = createMockLogger(); + + getHttpHandlerExtensionConfiguration({ requestHandler: handler, logger } as any); + + expect(handler.updateHttpClientConfig).toHaveBeenCalledWith("logger", logger); + }); + + it("injects logger into httpHandler when requestHandler is absent", () => { + const handler = createMockHandler(); + const logger = createMockLogger(); + + getHttpHandlerExtensionConfiguration({ httpHandler: handler, logger } as any); + + expect(handler.updateHttpClientConfig).toHaveBeenCalledWith("logger", logger); + }); + + it("prefers httpHandler over requestHandler", () => { + const httpHandler = createMockHandler(); + const requestHandler = createMockHandler(); + const logger = createMockLogger(); + + getHttpHandlerExtensionConfiguration({ httpHandler, requestHandler, logger } as any); + + expect(httpHandler.updateHttpClientConfig).toHaveBeenCalledWith("logger", logger); + expect(requestHandler.updateHttpClientConfig).not.toHaveBeenCalled(); + }); + + it("does not inject NoOpLogger", () => { + const handler = createMockHandler(); + + class NoOpLogger { + trace() {} + debug() {} + info() {} + warn() {} + error() {} + } + + getHttpHandlerExtensionConfiguration({ requestHandler: handler, logger: new NoOpLogger() } as any); + + expect(handler.updateHttpClientConfig).not.toHaveBeenCalled(); + }); + + it("does not inject when no logger is provided", () => { + const handler = createMockHandler(); + + getHttpHandlerExtensionConfiguration({ requestHandler: handler } as any); + + expect(handler.updateHttpClientConfig).not.toHaveBeenCalled(); + }); + + it("does not inject when no handler is present", () => { + const logger = createMockLogger(); + + // should not throw + getHttpHandlerExtensionConfiguration({ logger } as any); + }); + }); +}); diff --git a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts index d5bb23f2a2e..382e374ebf9 100644 --- a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts +++ b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts @@ -1,3 +1,5 @@ +import type { Logger } from "@smithy/types"; + import type { HttpHandler } from "../httpHandler"; /** @@ -17,6 +19,16 @@ export type HttpHandlerExtensionConfigType = httpHandler: HttpHandler; }>; +/** + * @internal + * + * Returns true if the logger is a no-op (all methods are empty), + * indicating the customer never explicitly configured one. + */ +const isNoOpLogger = (logger: Logger): boolean => { + return logger.constructor?.name === "NoOpLogger"; +}; + /** * Helper function to resolve default extension configuration from runtime config * @@ -25,6 +37,15 @@ export type HttpHandlerExtensionConfigType = export const getHttpHandlerExtensionConfiguration = ( runtimeConfig: HttpHandlerExtensionConfigType ) => { + const rc = runtimeConfig as HttpHandlerExtensionConfigType & { + requestHandler?: HttpHandler; + logger?: Logger; + }; + const handler = rc.httpHandler ?? rc.requestHandler; + if (handler && rc.logger && !isNoOpLogger(rc.logger)) { + handler.updateHttpClientConfig("logger" as keyof HandlerConfig, rc.logger as HandlerConfig[keyof HandlerConfig]); + } + return { setHttpHandler(handler: HttpHandler): void { runtimeConfig.httpHandler = handler; diff --git a/packages/node-http-handler/src/node-http-handler.spec.ts b/packages/node-http-handler/src/node-http-handler.spec.ts index 1867ee4b002..65cf6999c75 100644 --- a/packages/node-http-handler/src/node-http-handler.spec.ts +++ b/packages/node-http-handler/src/node-http-handler.spec.ts @@ -485,6 +485,59 @@ describe("NodeHttpHandler", () => { }); }); + describe("updateHttpClientConfig", () => { + it("uses nullish assignment for the logger key", async () => { + const handlerLogger = { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + const handler = new NodeHttpHandler({ logger: handlerLogger }); + + const clientLogger = { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + handler.updateHttpClientConfig("logger", clientLogger); + + // trigger config resolution + const request = new HttpRequest({ hostname: "localhost", method: "GET", protocol: "https:", path: "/" }); + try { + await handler.handle(request); + } catch { + // ignore request errors, we just need config to resolve + } + + const configs = handler.httpHandlerConfigs(); + expect(configs.logger).toBe(handlerLogger); + }); + + it("sets the logger when none was provided in handler options", async () => { + const handler = new NodeHttpHandler(); + + const clientLogger = { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + handler.updateHttpClientConfig("logger", clientLogger); + + const request = new HttpRequest({ hostname: "localhost", method: "GET", protocol: "https:", path: "/" }); + try { + await handler.handle(request); + } catch { + // ignore request errors + } + + const configs = handler.httpHandlerConfigs(); + expect(configs.logger).toBe(clientLogger); + }); + + it("overwrites for non-logger keys", async () => { + const handler = new NodeHttpHandler({ requestTimeout: 1000 }); + handler.updateHttpClientConfig("requestTimeout", 5000); + + const request = new HttpRequest({ hostname: "localhost", method: "GET", protocol: "https:", path: "/" }); + try { + await handler.handle(request); + } catch { + // ignore request errors + } + + const configs = handler.httpHandlerConfigs(); + expect(configs.requestTimeout).toBe(5000); + }); + }); + describe("checkSocketUsage", () => { beforeEach(() => { vi.spyOn(console, "warn").mockImplementation(vi.fn() as any); diff --git a/packages/node-http-handler/src/node-http-handler.ts b/packages/node-http-handler/src/node-http-handler.ts index 7990cd3addd..6d24e317cdb 100644 --- a/packages/node-http-handler/src/node-http-handler.ts +++ b/packages/node-http-handler/src/node-http-handler.ts @@ -325,6 +325,9 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf public updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void { this.config = undefined; this.configProvider = this.configProvider.then((config) => { + if (key === "logger") { + return { ...config, logger: config.logger ?? value }; + } return { ...config, [key]: value, diff --git a/packages/undici-http-handler/src/undici-http-handler.spec.ts b/packages/undici-http-handler/src/undici-http-handler.spec.ts index a11cc742c66..94a06ed7cd1 100644 --- a/packages/undici-http-handler/src/undici-http-handler.spec.ts +++ b/packages/undici-http-handler/src/undici-http-handler.spec.ts @@ -561,15 +561,22 @@ describe("UndiciHttpHandler", () => { expect(configs.logger).toBe(logger); }); - it("updates config", async () => { + it("does not overwrite an existing logger via updateHttpClientConfig", async () => { const logger = createMockLogger(); const updatedLogger = createMockLogger(); handler = new UndiciHttpHandler({ logger }); await handler.handle(createMockRequest()); handler.updateHttpClientConfig("logger", updatedLogger); - // Config is reset, need another request to resolve await handler.handle(createMockRequest()); - expect(handler.httpHandlerConfigs().logger).toBe(updatedLogger); + expect(handler.httpHandlerConfigs().logger).toBe(logger); + }); + + it("sets logger via updateHttpClientConfig when none was provided", async () => { + handler = new UndiciHttpHandler(); + const clientLogger = createMockLogger(); + handler.updateHttpClientConfig("logger", clientLogger); + await handler.handle(createMockRequest()); + expect(handler.httpHandlerConfigs().logger).toBe(clientLogger); }); it("retains existing dispatcher if undefined is passed", () => { diff --git a/packages/undici-http-handler/src/undici-http-handler.ts b/packages/undici-http-handler/src/undici-http-handler.ts index 0ad62a80adb..98f7a362c67 100644 --- a/packages/undici-http-handler/src/undici-http-handler.ts +++ b/packages/undici-http-handler/src/undici-http-handler.ts @@ -214,7 +214,11 @@ export class UndiciHttpHandler implements HttpHandler value: UndiciHttpHandlerOptions[K] ): void { if (key !== "dispatcher") { - (this.config as any)[key] = value; + if (key === "logger") { + (this.config as any)[key] ??= value; + } else { + (this.config as any)[key] = value; + } return; } From 66718983cf81ebe071db36561f285bdb5794ebc3 Mon Sep 17 00:00:00 2001 From: John Lwin Date: Tue, 21 Jul 2026 01:37:29 -0700 Subject: [PATCH 2/3] fix: cast logger value type in updateHttpClientConfig TypeScript strict mode cannot narrow `value` from the full `NodeHttpHandlerOptions[typeof key]` union when branching on `key === "logger"`. Add an explicit cast to fix the build:types target in CI. --- packages/node-http-handler/src/node-http-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-http-handler/src/node-http-handler.ts b/packages/node-http-handler/src/node-http-handler.ts index 6d24e317cdb..8e13c0968cb 100644 --- a/packages/node-http-handler/src/node-http-handler.ts +++ b/packages/node-http-handler/src/node-http-handler.ts @@ -326,7 +326,7 @@ or increase socketAcquisitionWarningTimeout=(millis) in the NodeHttpHandler conf this.config = undefined; this.configProvider = this.configProvider.then((config) => { if (key === "logger") { - return { ...config, logger: config.logger ?? value }; + return { ...config, logger: config.logger ?? (value as NodeHttpHandlerOptions["logger"]) }; } return { ...config, From 418fa747a19d76076cfe83c29dd43568779f6ec9 Mon Sep 17 00:00:00 2001 From: John Lwin Date: Thu, 23 Jul 2026 22:48:40 -0700 Subject: [PATCH 3/3] simplify logger injection per team decision --- .../pass-client-logger-to-request-handler.md | 10 ---- .../httpExtensionConfiguration.spec.ts | 50 +++---------------- .../extensions/httpExtensionConfiguration.ts | 21 +------- 3 files changed, 8 insertions(+), 73 deletions(-) diff --git a/.changeset/pass-client-logger-to-request-handler.md b/.changeset/pass-client-logger-to-request-handler.md index 48316e49673..9821a38dd5c 100644 --- a/.changeset/pass-client-logger-to-request-handler.md +++ b/.changeset/pass-client-logger-to-request-handler.md @@ -5,13 +5,3 @@ --- feat: pass client logger to request handlers - -Injects the client-level logger into the request handler during -`getHttpHandlerExtensionConfiguration`, so handler-emitted diagnostics -(e.g. socket exhaustion warnings) route through the customer's logger -instead of `console.warn`. - -Skips injection when the logger is `NoOpLogger` (the default), -preserving the existing console fallback for customers who never set a logger. -Handler-level loggers take precedence via nullish assignment in -`updateHttpClientConfig`. diff --git a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts index 0eee09fcde9..d77ee950624 100644 --- a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts +++ b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.spec.ts @@ -19,16 +19,7 @@ describe("getHttpHandlerExtensionConfiguration", () => { }); describe("client logger injection", () => { - it("injects logger into requestHandler when logger is explicitly set", () => { - const handler = createMockHandler(); - const logger = createMockLogger(); - - getHttpHandlerExtensionConfiguration({ requestHandler: handler, logger } as any); - - expect(handler.updateHttpClientConfig).toHaveBeenCalledWith("logger", logger); - }); - - it("injects logger into httpHandler when requestHandler is absent", () => { + it("passes logger to httpHandler via updateHttpClientConfig", () => { const handler = createMockHandler(); const logger = createMockLogger(); @@ -37,46 +28,19 @@ describe("getHttpHandlerExtensionConfiguration", () => { expect(handler.updateHttpClientConfig).toHaveBeenCalledWith("logger", logger); }); - it("prefers httpHandler over requestHandler", () => { - const httpHandler = createMockHandler(); - const requestHandler = createMockHandler(); + it("does not throw when no handler is present", () => { const logger = createMockLogger(); - getHttpHandlerExtensionConfiguration({ httpHandler, requestHandler, logger } as any); - - expect(httpHandler.updateHttpClientConfig).toHaveBeenCalledWith("logger", logger); - expect(requestHandler.updateHttpClientConfig).not.toHaveBeenCalled(); - }); - - it("does not inject NoOpLogger", () => { - const handler = createMockHandler(); - - class NoOpLogger { - trace() {} - debug() {} - info() {} - warn() {} - error() {} - } - - getHttpHandlerExtensionConfiguration({ requestHandler: handler, logger: new NoOpLogger() } as any); - - expect(handler.updateHttpClientConfig).not.toHaveBeenCalled(); + // should not throw + getHttpHandlerExtensionConfiguration({ logger } as any); }); - it("does not inject when no logger is provided", () => { + it("does not throw when no logger is provided", () => { const handler = createMockHandler(); - getHttpHandlerExtensionConfiguration({ requestHandler: handler } as any); + getHttpHandlerExtensionConfiguration({ httpHandler: handler } as any); - expect(handler.updateHttpClientConfig).not.toHaveBeenCalled(); - }); - - it("does not inject when no handler is present", () => { - const logger = createMockLogger(); - - // should not throw - getHttpHandlerExtensionConfiguration({ logger } as any); + expect(handler.updateHttpClientConfig).toHaveBeenCalledWith("logger", undefined); }); }); }); diff --git a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts index 382e374ebf9..2d3306fd7fd 100644 --- a/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts +++ b/packages/core/src/submodules/protocols/protocol-http/extensions/httpExtensionConfiguration.ts @@ -1,5 +1,3 @@ -import type { Logger } from "@smithy/types"; - import type { HttpHandler } from "../httpHandler"; /** @@ -19,16 +17,6 @@ export type HttpHandlerExtensionConfigType = httpHandler: HttpHandler; }>; -/** - * @internal - * - * Returns true if the logger is a no-op (all methods are empty), - * indicating the customer never explicitly configured one. - */ -const isNoOpLogger = (logger: Logger): boolean => { - return logger.constructor?.name === "NoOpLogger"; -}; - /** * Helper function to resolve default extension configuration from runtime config * @@ -37,14 +25,7 @@ const isNoOpLogger = (logger: Logger): boolean => { export const getHttpHandlerExtensionConfiguration = ( runtimeConfig: HttpHandlerExtensionConfigType ) => { - const rc = runtimeConfig as HttpHandlerExtensionConfigType & { - requestHandler?: HttpHandler; - logger?: Logger; - }; - const handler = rc.httpHandler ?? rc.requestHandler; - if (handler && rc.logger && !isNoOpLogger(rc.logger)) { - handler.updateHttpClientConfig("logger" as keyof HandlerConfig, rc.logger as HandlerConfig[keyof HandlerConfig]); - } + runtimeConfig.httpHandler?.updateHttpClientConfig("logger" as keyof HandlerConfig, (runtimeConfig as any).logger); return { setHttpHandler(handler: HttpHandler): void {