Skip to content

Commit f783a42

Browse files
authored
chore(middleware-user-agent): update to user agent 2.1 spec (#6536)
1 parent 2a50045 commit f783a42

19 files changed

+220
-12
lines changed

packages/core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
},
8080
"license": "Apache-2.0",
8181
"dependencies": {
82+
"@aws-sdk/types": "*",
8283
"@smithy/core": "^2.4.7",
8384
"@smithy/node-config-provider": "^3.1.8",
8485
"@smithy/property-provider": "^3.1.7",
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./emitWarningIfUnsupportedVersion";
2+
export * from "./setFeature";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { AwsHandlerExecutionContext } from "@aws-sdk/types";
2+
3+
import { setFeature } from "./setFeature";
4+
5+
describe(setFeature.name, () => {
6+
it("creates the context object path if needed", () => {
7+
const context: AwsHandlerExecutionContext = {};
8+
setFeature(context, "ACCOUNT_ID_ENDPOINT", "O");
9+
});
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { AwsHandlerExecutionContext, AwsSdkFeatures } from "@aws-sdk/types";
2+
3+
/**
4+
* @internal
5+
* Indicates to the request context that a given feature is active.
6+
*
7+
* @param context - handler execution context.
8+
* @param feature - readable name of feature.
9+
* @param value - encoding value of feature. This is required because the
10+
* specification asks the SDK not to include a runtime lookup of all
11+
* the feature identifiers.
12+
*/
13+
export function setFeature<F extends keyof AwsSdkFeatures>(
14+
context: AwsHandlerExecutionContext,
15+
feature: F,
16+
value: AwsSdkFeatures[F]
17+
) {
18+
if (!context.__aws_sdk_context) {
19+
context.__aws_sdk_context = {
20+
features: {},
21+
};
22+
} else if (!context.__aws_sdk_context.features) {
23+
context.__aws_sdk_context.features = {};
24+
}
25+
context.__aws_sdk_context.features![feature] = value;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { encodeFeatures } from "./encode-features";
2+
3+
describe(encodeFeatures.name, () => {
4+
it("encodes empty features", () => {
5+
expect(encodeFeatures({})).toEqual("");
6+
});
7+
8+
it("encodes features", () => {
9+
expect(
10+
encodeFeatures({
11+
A: "A",
12+
z: "z",
13+
} as any)
14+
).toEqual("A,z");
15+
});
16+
17+
it("drops values that would exceed 1024 bytes", () => {
18+
expect(
19+
encodeFeatures({
20+
A: "A".repeat(512),
21+
B: "B".repeat(511),
22+
z: "z",
23+
} as any)
24+
).toEqual("A".repeat(512) + "," + "B".repeat(511));
25+
});
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { AwsSdkFeatures } from "@aws-sdk/types";
2+
3+
const BYTE_LIMIT = 1024;
4+
5+
/**
6+
* @internal
7+
*/
8+
export function encodeFeatures(features: AwsSdkFeatures): string {
9+
let buffer = "";
10+
11+
// currently all possible values are 1 byte,
12+
// so string length is used.
13+
14+
for (const key in features) {
15+
const val = features[key as keyof typeof features]!;
16+
if (buffer.length + val!.length + 1 <= BYTE_LIMIT) {
17+
if (buffer.length) {
18+
buffer += "," + val;
19+
} else {
20+
buffer += val;
21+
}
22+
continue;
23+
}
24+
break;
25+
}
26+
27+
return buffer;
28+
}

packages/middleware-user-agent/src/middleware-user-agent.integ.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("middleware-user-agent", () => {
1414
requireRequestsFrom(client).toMatch({
1515
headers: {
1616
"x-amz-user-agent": /aws-sdk-js\/[\d\.]+/,
17-
"user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+/,
17+
"user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+ (.*?)m\//,
1818
},
1919
});
2020
await client.getUserDetails({

packages/middleware-user-agent/src/user-agent-middleware.spec.ts

+32
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,38 @@ describe("userAgentMiddleware", () => {
8989
expect(sdkUserAgent).toEqual(expect.stringContaining("custom_ua/abc"));
9090
});
9191

92+
describe("features", () => {
93+
it("should collect features from the context", async () => {
94+
const middleware = userAgentMiddleware({
95+
defaultUserAgentProvider: async () => [
96+
["default_agent", "1.0.0"],
97+
["aws-sdk-js", "1.0.0"],
98+
],
99+
runtime: "node",
100+
userAgentAppId: async () => undefined,
101+
});
102+
103+
const handler = middleware(mockNextHandler, {
104+
__aws_sdk_context: {
105+
features: {
106+
"0": "0",
107+
"9": "9",
108+
A: "A",
109+
B: "B",
110+
y: "y",
111+
z: "z",
112+
"+": "+",
113+
"/": "/",
114+
},
115+
},
116+
});
117+
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
118+
expect(mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT]).toEqual(
119+
expect.stringContaining(`m/0,9,A,B,y,z,+,/`)
120+
);
121+
});
122+
});
123+
92124
describe("should sanitize the SDK user agent string", () => {
93125
const cases: { ua: UserAgentPair; expected: string }[] = [
94126
{ ua: ["/name", "1.0.0"], expected: "name/1.0.0" },

packages/middleware-user-agent/src/user-agent-middleware.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { AwsHandlerExecutionContext } from "@aws-sdk/types";
12
import { getUserAgentPrefix } from "@aws-sdk/util-endpoints";
23
import { HttpRequest } from "@smithy/protocol-http";
34
import {
@@ -22,6 +23,7 @@ import {
2223
USER_AGENT,
2324
X_AMZ_USER_AGENT,
2425
} from "./constants";
26+
import { encodeFeatures } from "./encode-features";
2527

2628
/**
2729
* Build user agent header sections from:
@@ -39,14 +41,22 @@ export const userAgentMiddleware =
3941
(options: UserAgentResolvedConfig) =>
4042
<Output extends MetadataBearer>(
4143
next: BuildHandler<any, any>,
42-
context: HandlerExecutionContext
44+
context: HandlerExecutionContext | AwsHandlerExecutionContext
4345
): BuildHandler<any, any> =>
4446
async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
4547
const { request } = args;
46-
if (!HttpRequest.isInstance(request)) return next(args);
48+
if (!HttpRequest.isInstance(request)) {
49+
return next(args);
50+
}
4751
const { headers } = request;
4852
const userAgent = context?.userAgent?.map(escapeUserAgent) || [];
49-
let defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
53+
const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
54+
const awsContext = context as AwsHandlerExecutionContext;
55+
defaultUserAgent.push(
56+
`m/${encodeFeatures(
57+
Object.assign({}, context.__smithy_context?.features, awsContext.__aws_sdk_context?.features)
58+
)}`
59+
);
5060
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
5161
const appId = await options.userAgentAppId();
5262
if (appId) {

packages/types/src/feature-ids.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @internal
3+
*/
4+
export type AwsSdkFeatures = Partial<{
5+
RESOURCE_MODEL: "A";
6+
WAITER: "B";
7+
PAGINATOR: "C";
8+
RETRY_MODE_LEGACY: "D";
9+
RETRY_MODE_STANDARD: "E";
10+
RETRY_MODE_ADAPTIVE: "F";
11+
// S3_TRANSFER: "G"; // not applicable.
12+
// S3_CRYPTO_V1N: "H"; // not applicable.
13+
// S3_CRYPTO_V2: "I"; // not applicable.
14+
S3_EXPRESS_BUCKET: "J";
15+
S3_ACCESS_GRANTS: "K";
16+
GZIP_REQUEST_COMPRESSION: "L";
17+
PROTOCOL_RPC_V2_CBOR: "M";
18+
ENDPOINT_OVERRIDE: "N";
19+
ACCOUNT_ID_ENDPOINT: "O";
20+
ACCOUNT_ID_MODE_PREFERRED: "P";
21+
ACCOUNT_ID_MODE_DISABLED: "Q";
22+
ACCOUNT_ID_MODE_REQUIRED: "R";
23+
SIGV4A_SIGNING: "S";
24+
RESOLVED_ACCOUNT_ID: "T";
25+
FLEXIBLE_CHECKSUMS_REQ_CRC32: "U";
26+
FLEXIBLE_CHECKSUMS_REQ_CRC32C: "V";
27+
FLEXIBLE_CHECKSUMS_REQ_CRC64: "W";
28+
FLEXIBLE_CHECKSUMS_REQ_SHA1: "X";
29+
FLEXIBLE_CHECKSUMS_REQ_SHA256: "Y";
30+
FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED: "Z";
31+
FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED: "a";
32+
FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED: "b";
33+
FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED: "c";
34+
DDB_MAPPER: "d";
35+
CREDENTIALS_CODE: "e";
36+
// CREDENTIALS_JVM_SYSTEM_PROPERTIES: "f"; // not applicable.
37+
CREDENTIALS_ENV_VARS: "g";
38+
CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN: "h";
39+
CREDENTIALS_STS_ASSUME_ROLE: "i";
40+
CREDENTIALS_STS_ASSUME_ROLE_SAML: "j";
41+
CREDENTIALS_STS_ASSUME_ROLE_WEB_ID: "k";
42+
CREDENTIALS_STS_FEDERATION_TOKEN: "l";
43+
CREDENTIALS_STS_SESSION_TOKEN: "m";
44+
CREDENTIALS_PROFILE: "n";
45+
CREDENTIALS_PROFILE_SOURCE_PROFILE: "o";
46+
CREDENTIALS_PROFILE_NAMED_PROVIDER: "p";
47+
CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN: "q";
48+
CREDENTIALS_PROFILE_SSO: "r";
49+
CREDENTIALS_SSO: "s";
50+
CREDENTIALS_PROFILE_SSO_LEGACY: "t";
51+
CREDENTIALS_SSO_LEGACY: "u";
52+
CREDENTIALS_PROFILE_PROCESS: "v";
53+
CREDENTIALS_PROCESS: "w";
54+
CREDENTIALS_BOTO2_CONFIG_FILE: "x";
55+
CREDENTIALS_AWS_SDK_STORE: "y";
56+
CREDENTIALS_HTTP: "z";
57+
CREDENTIALS_IMDS: "0";
58+
}>;

packages/types/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from "./encode";
1212
export * from "./endpoint";
1313
export * from "./eventStream";
1414
export * from "./extensions";
15+
export * from "./feature-ids";
1516
export * from "./http";
1617
export * from "./identity";
1718
export * from "./logger";

packages/types/src/middleware.ts

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { HandlerExecutionContext } from "@smithy/types";
2+
3+
import { AwsSdkFeatures } from "./feature-ids";
4+
15
export {
26
AbsoluteLocation,
37
BuildHandler,
@@ -38,3 +42,14 @@ export {
3842
Step,
3943
Terminalware,
4044
} from "@smithy/types";
45+
46+
/**
47+
* @internal
48+
* Contains reserved keys for AWS SDK internal usage of the
49+
* handler execution context object.
50+
*/
51+
export interface AwsHandlerExecutionContext extends HandlerExecutionContext {
52+
__aws_sdk_context?: {
53+
features?: AwsSdkFeatures;
54+
};
55+
}

packages/util-user-agent-browser/src/index.native.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ it("should response basic browser default user agent", async () => {
66
jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(undefined);
77
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })();
88
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
9-
expect(userAgent[1]).toEqual(["ua", "2.0"]);
9+
expect(userAgent[1]).toEqual(["ua", "2.1"]);
1010
expect(userAgent[2]).toEqual(["os/other"]);
1111
expect(userAgent[3]).toEqual(["lang/js"]);
1212
expect(userAgent[4]).toEqual(["md/rn"]);

packages/util-user-agent-browser/src/index.native.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const defaultUserAgent =
1515
// sdk-metadata
1616
["aws-sdk-js", clientVersion],
1717
// ua-metadata
18-
["ua", "2.0"],
18+
["ua", "2.1"],
1919
// os-metadata
2020
["os/other"],
2121
// language-metadata

packages/util-user-agent-browser/src/index.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe("defaultUserAgent", () => {
1919
it("should populate metrics", async () => {
2020
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
2121
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
22-
expect(userAgent[1]).toEqual(["ua", "2.0"]);
22+
expect(userAgent[1]).toEqual(["ua", "2.1"]);
2323
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
2424
expect(userAgent[3]).toEqual(["lang/js"]);
2525
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
@@ -47,4 +47,4 @@ describe("defaultUserAgent", () => {
4747
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
4848
expect(userAgent.length).toBe(6);
4949
});
50-
});
50+
});

packages/util-user-agent-browser/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const defaultUserAgent =
2424
// sdk-metadata
2525
["aws-sdk-js", clientVersion],
2626
// ua-metadata
27-
["ua", "2.0"],
27+
["ua", "2.1"],
2828
// os-metadata
2929
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
3030
// language-metadata

packages/util-user-agent-node/src/defaultUserAgent.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe("createDefaultUserAgentProvider", () => {
3939

4040
const basicUserAgent: UserAgent = [
4141
["aws-sdk-js", "0.1.0"],
42-
["ua", "2.0"],
42+
["ua", "2.1"],
4343
["api/s3", "0.1.0"],
4444
["os/darwin", "19.6.0"],
4545
["lang/js"],

packages/util-user-agent-node/src/defaultUserAgent.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const createDefaultUserAgentProvider = ({ serviceId, clientVersion }: Def
2626
// sdk-metadata
2727
["aws-sdk-js", clientVersion],
2828
// ua-metadata
29-
["ua", "2.0"],
29+
["ua", "2.1"],
3030
// os-metadata
3131
[`os/${platform()}`, release()],
3232
// language-metadata
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export * from "./defaultUserAgent";
2-
export * from "./nodeAppIdConfigOptions";
2+
export * from "./nodeAppIdConfigOptions";

0 commit comments

Comments
 (0)