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+
}>;

0 commit comments

Comments
 (0)