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

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: 26 additions & 0 deletions
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+
}
Lines changed: 26 additions & 0 deletions
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+
});
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 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

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: 32 additions & 0 deletions
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

Lines changed: 13 additions & 3 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 { 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

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)