Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/stale-phones-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/middleware-endpoint": minor
---

handle clientContextParam collisions with builtin config keys
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe("local model integration test for cbor eventstreams", () => {
it("should read and write cbor event streams", async () => {
const client = new XYZService({
endpoint: "https://localhost",
});
apiKey: { apiKey: "test-api-key" },
} as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there should be no as any here. This test is from a user perspective (integ test means calls user-facing public APIs only), so the types must align with the user intention.


const body = cbor.serialize({
id: "alpha",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,31 @@ describe(createConfigValueProvider.name, () => {
expect(await createConfigValueProvider("v1", "endpoint", config)()).toEqual(sampleUrl);
expect(await createConfigValueProvider("v2", "endpoint", config)()).toEqual(sampleUrl);
});

it("should prioritize clientContextParams over direct properties", async () => {
const config = {
apiKey: "direct-api-key",
clientContextParams: {
apiKey: "nested-api-key",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although conflicts don't matter at the unit level (and this code is not aware of what is considered a conflict),

change this test key to something else that doesn't conflict, like stage: string = "beta" | "prod". This implies that apiKey is a string at the base config level, but it isn't (when present).

},
};
expect(await createConfigValueProvider("apiKey", "apiKey", config, true)()).toEqual("nested-api-key");
});

it("should fall back to direct property when clientContextParams is not provided", async () => {
const config = {
customParam: "direct-value",
};
expect(await createConfigValueProvider("customParam", "customParam", config)()).toEqual("direct-value");
});

it("should fall back to direct property when clientContextParams exists but param is not in it", async () => {
const config = {
customParam: "direct-value",
clientContextParams: {
otherParam: "other-value",
},
};
expect(await createConfigValueProvider("customParam", "customParam", config)()).toEqual("direct-value");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,29 @@ import type { Endpoint, EndpointV2 } from "@smithy/types";
* it will most likely not contain the config
* value, but we use it as a fallback.
* @param config - container of the config values.
* @param isClientContextParam - whether this is a client context parameter.
*
* @returns async function that will resolve with the value.
*/
export const createConfigValueProvider = <Config extends Record<string, unknown>>(
configKey: string,
canonicalEndpointParamKey: string,
config: Config
config: Config,
isClientContextParam = false
) => {
const configProvider = async () => {
const configValue: unknown = config[configKey] ?? config[canonicalEndpointParamKey];
let configValue: unknown;

if (isClientContextParam) {
// For client context parameters, check clientContextParams first
const clientContextParams = config.clientContextParams as Record<string, unknown> | undefined;
const nestedValue: unknown = clientContextParams?.[canonicalEndpointParamKey];
configValue = nestedValue ?? config[configKey] ?? config[canonicalEndpointParamKey];
} else {
// For built-in parameters and other config properties
configValue = config[configKey] ?? config[canonicalEndpointParamKey];
}

if (typeof configValue === "function") {
return configValue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ export const resolveParams = async <
break;
case "clientContextParams":
case "builtInParams":
endpointParams[name] = await createConfigValueProvider<Config>(instruction.name, name, clientConfig)();
endpointParams[name] = await createConfigValueProvider<Config>(
instruction.name,
name,
clientConfig,
instruction.type !== "builtInParams"
)();
break;
case "operationContextParams":
endpointParams[name] = instruction.get(commandInput);
Expand Down
12 changes: 8 additions & 4 deletions packages/util-retry/src/retries.integ.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ describe("retries", () => {
it("should retry throttling and transient-error status codes", async () => {
const client = new XYZService({
endpoint: "https://localhost/nowhere",
});
apiKey: { apiKey: "test-api-key" },
} as any);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove as any

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I removed any it was throwing a type incompatibility error.


requireRequestsFrom(client)
.toMatch({
Expand Down Expand Up @@ -50,7 +51,8 @@ describe("retries", () => {
it("should retry when a retryable trait is modeled", async () => {
const client = new XYZService({
endpoint: "https://localhost/nowhere",
});
apiKey: { apiKey: "test-api-key" },
} as any);

requireRequestsFrom(client)
.toMatch({
Expand Down Expand Up @@ -80,7 +82,8 @@ describe("retries", () => {
it("should retry retryable trait with throttling", async () => {
const client = new XYZService({
endpoint: "https://localhost/nowhere",
});
apiKey: { apiKey: "test-api-key" },
} as any);

requireRequestsFrom(client)
.toMatch({
Expand Down Expand Up @@ -110,7 +113,8 @@ describe("retries", () => {
it("should not retry if the error is not modeled with retryable trait and is not otherwise retryable", async () => {
const client = new XYZService({
endpoint: "https://localhost/nowhere",
});
apiKey: { apiKey: "test-api-key" },
} as any);

requireRequestsFrom(client)
.toMatch({
Expand Down
18 changes: 7 additions & 11 deletions private/my-local-model-schema/src/XYZServiceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ import {
import { getContentLengthPlugin } from "@smithy/middleware-content-length";
import {
type EndpointInputConfig,
type EndpointRequiredInputConfig,
type EndpointRequiredResolvedConfig,
type EndpointResolvedConfig,
resolveEndpointConfig,
resolveEndpointRequiredConfig,
} from "@smithy/middleware-endpoint";
import {
type RetryInputConfig,
Expand Down Expand Up @@ -203,7 +200,6 @@ export type XYZServiceClientConfigType = Partial<__SmithyConfiguration<__HttpHan
ClientDefaults &
RetryInputConfig &
EndpointInputConfig<EndpointParameters> &
EndpointRequiredInputConfig &
EventStreamSerdeInputConfig &
HttpAuthSchemeInputConfig &
ClientInputEndpointParameters;
Expand All @@ -222,7 +218,6 @@ export type XYZServiceClientResolvedConfigType = __SmithyResolvedConfiguration<_
RuntimeExtensionsConfig &
RetryResolvedConfig &
EndpointResolvedConfig<EndpointParameters> &
EndpointRequiredResolvedConfig &
EventStreamSerdeResolvedConfig &
HttpAuthSchemeResolvedConfig &
ClientResolvedEndpointParameters;
Expand Down Expand Up @@ -255,19 +250,20 @@ export class XYZServiceClient extends __Client<
const _config_1 = resolveClientEndpointParameters(_config_0);
const _config_2 = resolveRetryConfig(_config_1);
const _config_3 = resolveEndpointConfig(_config_2);
const _config_4 = resolveEndpointRequiredConfig(_config_3);
const _config_5 = resolveEventStreamSerdeConfig(_config_4);
const _config_6 = resolveHttpAuthSchemeConfig(_config_5);
const _config_7 = resolveRuntimeExtensions(_config_6, configuration?.extensions || []);
this.config = _config_7;
const _config_4 = resolveEventStreamSerdeConfig(_config_3);
const _config_5 = resolveHttpAuthSchemeConfig(_config_4);
const _config_6 = resolveRuntimeExtensions(_config_5, configuration?.extensions || []);
this.config = _config_6;
this.middlewareStack.use(getSchemaSerdePlugin(this.config));
this.middlewareStack.use(getRetryPlugin(this.config));
this.middlewareStack.use(getContentLengthPlugin(this.config));
this.middlewareStack.use(
getHttpAuthSchemeEndpointRuleSetPlugin(this.config, {
httpAuthSchemeParametersProvider: defaultXYZServiceHttpAuthSchemeParametersProvider,
identityProviderConfigProvider: async (config: XYZServiceClientResolvedConfig) =>
new DefaultIdentityProviderConfig({}),
new DefaultIdentityProviderConfig({
"smithy.api#httpApiKeyAuth": config.apiKey,
}),
})
);
this.middlewareStack.use(getHttpSigningPlugin(this.config));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// smithy-typescript generated code
import type { HttpAuthScheme } from "@smithy/types";
import { type HttpAuthScheme, ApiKeyIdentity, ApiKeyIdentityProvider } from "@smithy/types";

import type { XYZServiceHttpAuthSchemeProvider } from "./httpAuthSchemeProvider";

Expand All @@ -11,6 +11,8 @@ export interface HttpAuthExtensionConfiguration {
httpAuthSchemes(): HttpAuthScheme[];
setHttpAuthSchemeProvider(httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider): void;
httpAuthSchemeProvider(): XYZServiceHttpAuthSchemeProvider;
setApiKey(apiKey: ApiKeyIdentity | ApiKeyIdentityProvider): void;
apiKey(): ApiKeyIdentity | ApiKeyIdentityProvider | undefined;
}

/**
Expand All @@ -19,6 +21,7 @@ export interface HttpAuthExtensionConfiguration {
export type HttpAuthRuntimeConfig = Partial<{
httpAuthSchemes: HttpAuthScheme[];
httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider;
apiKey: ApiKeyIdentity | ApiKeyIdentityProvider;
}>;

/**
Expand All @@ -29,6 +32,7 @@ export const getHttpAuthExtensionConfiguration = (
): HttpAuthExtensionConfiguration => {
const _httpAuthSchemes = runtimeConfig.httpAuthSchemes!;
let _httpAuthSchemeProvider = runtimeConfig.httpAuthSchemeProvider!;
let _apiKey = runtimeConfig.apiKey;
return {
setHttpAuthScheme(httpAuthScheme: HttpAuthScheme): void {
const index = _httpAuthSchemes.findIndex((scheme) => scheme.schemeId === httpAuthScheme.schemeId);
Expand All @@ -47,6 +51,12 @@ export const getHttpAuthExtensionConfiguration = (
httpAuthSchemeProvider(): XYZServiceHttpAuthSchemeProvider {
return _httpAuthSchemeProvider;
},
setApiKey(apiKey: ApiKeyIdentity | ApiKeyIdentityProvider): void {
_apiKey = apiKey;
},
apiKey(): ApiKeyIdentity | ApiKeyIdentityProvider | undefined {
return _apiKey;
},
};
};

Expand All @@ -57,5 +67,6 @@ export const resolveHttpAuthRuntimeConfig = (config: HttpAuthExtensionConfigurat
return {
httpAuthSchemes: config.httpAuthSchemes(),
httpAuthSchemeProvider: config.httpAuthSchemeProvider(),
apiKey: config.apiKey(),
};
};
45 changes: 34 additions & 11 deletions private/my-local-model-schema/src/auth/httpAuthSchemeProvider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// smithy-typescript generated code
import type {
HandlerExecutionContext,
HttpAuthOption,
HttpAuthScheme,
HttpAuthSchemeParameters,
HttpAuthSchemeParametersProvider,
HttpAuthSchemeProvider,
Provider,
import { doesIdentityRequireRefresh, isIdentityExpired, memoizeIdentityProvider } from "@smithy/core";
import {
type HandlerExecutionContext,
type HttpAuthOption,
type HttpAuthScheme,
type HttpAuthSchemeParameters,
type HttpAuthSchemeParametersProvider,
type HttpAuthSchemeProvider,
type Provider,
ApiKeyIdentity,
ApiKeyIdentityProvider,
HttpApiKeyAuthLocation,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these new imports look like types. Change the codegen part to use addTypeImport() instead, or if imported via symbol, ensure the symbol has the typeOnly property.

} from "@smithy/types";
import { getSmithyContext, normalizeProvider } from "@smithy/util-middleware";

Expand Down Expand Up @@ -41,9 +45,16 @@ export const defaultXYZServiceHttpAuthSchemeParametersProvider = async (
};
};

function createSmithyApiNoAuthHttpAuthOption(authParameters: XYZServiceHttpAuthSchemeParameters): HttpAuthOption {
function createSmithyApiHttpApiKeyAuthHttpAuthOption(
authParameters: XYZServiceHttpAuthSchemeParameters
): HttpAuthOption {
return {
schemeId: "smithy.api#noAuth",
schemeId: "smithy.api#httpApiKeyAuth",
signingProperties: {
name: "X-Api-Key",
in: HttpApiKeyAuthLocation.HEADER,
scheme: undefined,
},
};
}

Expand All @@ -59,7 +70,7 @@ export const defaultXYZServiceHttpAuthSchemeProvider: XYZServiceHttpAuthSchemePr
const options: HttpAuthOption[] = [];
switch (authParameters.operation) {
default: {
options.push(createSmithyApiNoAuthHttpAuthOption(authParameters));
options.push(createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters));
}
}
return options;
Expand Down Expand Up @@ -88,6 +99,11 @@ export interface HttpAuthSchemeInputConfig {
* @internal
*/
httpAuthSchemeProvider?: XYZServiceHttpAuthSchemeProvider;

/**
* The API key to use when making requests.
*/
apiKey?: ApiKeyIdentity | ApiKeyIdentityProvider;
}

/**
Expand All @@ -113,6 +129,11 @@ export interface HttpAuthSchemeResolvedConfig {
* @internal
*/
readonly httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider;

/**
* The API key to use when making requests.
*/
readonly apiKey?: ApiKeyIdentityProvider;
}

/**
Expand All @@ -121,7 +142,9 @@ export interface HttpAuthSchemeResolvedConfig {
export const resolveHttpAuthSchemeConfig = <T>(
config: T & HttpAuthSchemeInputConfig
): T & HttpAuthSchemeResolvedConfig => {
const apiKey = memoizeIdentityProvider(config.apiKey, isIdentityExpired, doesIdentityRequireRefresh);
return Object.assign(config, {
authSchemePreference: normalizeProvider(config.authSchemePreference ?? []),
apiKey,
}) as T & HttpAuthSchemeResolvedConfig;
};
Loading