Skip to content

Commit 521f9fe

Browse files
committed
chore(middleware-user-agent): update to user agent 2.1 spec
1 parent 021f218 commit 521f9fe

File tree

18 files changed

+217
-9
lines changed

18 files changed

+217
-9
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
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",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./emitWarningIfUnsupportedVersion";
2+
export * from "./setFeature";
Lines changed: 10 additions & 0 deletions
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+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { AwsHandlerExecutionContext, AwsSdkFeatures } from "@aws-sdk/types";
2+
3+
/**
4+
* @internal
5+
* Sets the feature for the request context to be read in the user agent
6+
* middleware.
7+
*
8+
* @param context - handler execution context.
9+
* @param feature - readable name of feature.
10+
* @param value - encoding value of feature. This is required because the
11+
* specification asks the SDK not to include a runtime lookup of all
12+
* the feature identifiers.
13+
*/
14+
export function setFeature<F extends keyof AwsSdkFeatures>(
15+
context: AwsHandlerExecutionContext,
16+
feature: F,
17+
value: AwsSdkFeatures[F]
18+
) {
19+
if (!context.__aws_sdk_context) {
20+
context.__aws_sdk_context = {
21+
features: {},
22+
};
23+
} else if (!context.__aws_sdk_context.features) {
24+
context.__aws_sdk_context.features = {};
25+
}
26+
context.__aws_sdk_context.features![feature] = value;
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { encodeMetrics } from "./encode-metrics";
2+
3+
describe(encodeMetrics.name, () => {
4+
it("encodes empty metrics", () => {
5+
expect(encodeMetrics({})).toEqual("");
6+
});
7+
8+
it("encodes metrics", () => {
9+
expect(
10+
encodeMetrics({
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+
encodeMetrics({
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+
});
Lines changed: 28 additions & 0 deletions
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 encodeMetrics(metrics: 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 metrics) {
15+
const val = metrics[key as keyof typeof metrics]!;
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,37 @@ describe("userAgentMiddleware", () => {
5050
);
5151
});
5252

53+
describe("metrics", () => {
54+
it("should collect metrics from the context", async () => {
55+
const middleware = userAgentMiddleware({
56+
defaultUserAgentProvider: async () => [
57+
["default_agent", "1.0.0"],
58+
["aws-sdk-js", "1.0.0"],
59+
],
60+
runtime: "node",
61+
});
62+
63+
const handler = middleware(mockNextHandler, {
64+
__aws_sdk_context: {
65+
features: {
66+
"0": "0",
67+
"9": "9",
68+
A: "A",
69+
B: "B",
70+
y: "y",
71+
z: "z",
72+
"+": "+",
73+
"/": "/",
74+
},
75+
},
76+
});
77+
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
78+
expect(mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT]).toEqual(
79+
expect.stringContaining(`m/0,9,A,B,y,z,+,/`)
80+
);
81+
});
82+
});
83+
5384
describe("should sanitize the SDK user agent string", () => {
5485
const cases: { ua: UserAgentPair; expected: string }[] = [
5586
{ ua: ["/name", "1.0.0"], expected: "name/1.0.0" },

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

Lines changed: 12 additions & 2 deletions
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 { encodeMetrics } from "./encode-metrics";
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) || [];
4953
const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
54+
const awsContext = context as AwsHandlerExecutionContext;
55+
defaultUserAgent.push(
56+
`m/${encodeMetrics(
57+
Object.assign({}, context.__smithy_context?.features, awsContext.__aws_sdk_context?.features)
58+
)}`
59+
);
5060
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
5161
const prefix = getUserAgentPrefix();
5262

packages/types/src/feature-ids.ts

Lines changed: 58 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 15 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ it("should populate metrics", async () => {
77
jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
88
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })();
99
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
10-
expect(userAgent[1]).toEqual(["ua", "2.0"]);
10+
expect(userAgent[1]).toEqual(["ua", "2.1"]);
1111
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
1212
expect(userAgent[3]).toEqual(["lang/js"]);
1313
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const defaultUserAgent =
2020
// sdk-metadata
2121
["aws-sdk-js", clientVersion],
2222
// ua-metadata
23-
["ua", "2.0"],
23+
["ua", "2.1"],
2424
// os-metadata
2525
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
2626
// language-metadata

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe("defaultUserAgent", () => {
4343

4444
const basicUserAgent: UserAgent = [
4545
["aws-sdk-js", "0.1.0"],
46-
["ua", "2.0"],
46+
["ua", "2.1"],
4747
["api/s3", "0.1.0"],
4848
["os/darwin", "19.6.0"],
4949
["lang/js"],

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const defaultUserAgent = ({ serviceId, clientVersion }: DefaultUserAgentO
3131
// sdk-metadata
3232
["aws-sdk-js", clientVersion],
3333
// ua-metadata
34-
["ua", "2.0"],
34+
["ua", "2.1"],
3535
// os-metadata
3636
[`os/${platform()}`, release()],
3737
// language-metadata

0 commit comments

Comments
 (0)