Skip to content

Commit

Permalink
chore(middleware-user-agent): update to user agent 2.1 spec
Browse files Browse the repository at this point in the history
  • Loading branch information
kuhe committed Oct 2, 2024
1 parent 3c2e59c commit 9dcba0d
Show file tree
Hide file tree
Showing 17 changed files with 155 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "*",
"@smithy/core": "^2.4.7",
"@smithy/node-config-provider": "^3.1.8",
"@smithy/property-provider": "^3.1.7",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/submodules/client/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./emitWarningIfUnsupportedVersion";
export * from "./setFeature";
10 changes: 10 additions & 0 deletions packages/core/src/submodules/client/setFeature.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AwsHandlerExecutionContext } from "@aws-sdk/types";

import { setFeature } from "./setFeature";

describe(setFeature.name, () => {
it("creates the context object path if needed", () => {
const context: AwsHandlerExecutionContext = {};
setFeature(context, "ACCOUNT_ID_ENDPOINT", "O");
});
});
27 changes: 27 additions & 0 deletions packages/core/src/submodules/client/setFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { AwsHandlerExecutionContext, AwsSdkFeatures } from "@aws-sdk/types";

/**
* @internal
* Sets the feature for the request context to be read in the user agent
* middleware.
*
* @param context - handler execution context.
* @param feature - readable name of feature.
* @param value - encoding value of feature. This is required because the
* specification asks the SDK not to include a runtime lookup of all
* the feature identifiers.
*/
export function setFeature<F extends keyof AwsSdkFeatures>(
context: AwsHandlerExecutionContext,
feature: F,
value: AwsSdkFeatures[F]
) {
if (!context.__aws_sdk_context) {
context.__aws_sdk_context = {
features: {},
};
} else if (!context.__aws_sdk_context.features) {
context.__aws_sdk_context.features = {};
}
context.__aws_sdk_context.features![feature] = value;
}
26 changes: 26 additions & 0 deletions packages/middleware-user-agent/src/encode-metrics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { encodeMetrics } from "./encode-metrics";

describe(encodeMetrics.name, () => {
it("encodes empty metrics", () => {
expect(encodeMetrics({})).toEqual("");
});

it("encodes metrics", () => {
expect(
encodeMetrics({
A: "A",
z: "z",
} as any)
).toEqual("A,z");
});

it("drops values that would exceed 1024 bytes", () => {
expect(
encodeMetrics({
A: "A".repeat(512),
B: "B".repeat(511),
z: "z",
} as any)
).toEqual("A".repeat(512) + "," + "B".repeat(511));
});
});
28 changes: 28 additions & 0 deletions packages/middleware-user-agent/src/encode-metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { AwsSdkFeatures } from "@aws-sdk/types";

const BYTE_LIMIT = 1024;

/**
* @internal
*/
export function encodeMetrics(metrics: AwsSdkFeatures): string {
let buffer = "";

// currently all possible values are 1 byte,
// so string length is used.

for (const key in metrics) {
const val = metrics[key as keyof typeof metrics]!;
if (buffer.length + val!.length + 1 <= BYTE_LIMIT) {
if (buffer.length) {
buffer += "," + val;
} else {
buffer += val;
}
continue;
}
break;
}

return buffer;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("middleware-user-agent", () => {
requireRequestsFrom(client).toMatch({
headers: {
"x-amz-user-agent": /aws-sdk-js\/[\d\.]+/,
"user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+/,
"user-agent": /aws-sdk-js\/[\d\.]+ (.*?)lang\/js md\/nodejs\#[\d\.]+ (.*?)api\/(.+)\#[\d\.]+ (.*?)m\//,
},
});
await client.getUserDetails({
Expand Down
31 changes: 31 additions & 0 deletions packages/middleware-user-agent/src/user-agent-middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,37 @@ describe("userAgentMiddleware", () => {
);
});

describe("metrics", () => {
it("should collect metrics from the context", async () => {
const middleware = userAgentMiddleware({
defaultUserAgentProvider: async () => [
["default_agent", "1.0.0"],
["aws-sdk-js", "1.0.0"],
],
runtime: "node",
});

const handler = middleware(mockNextHandler, {
__aws_sdk_context: {
features: {
"0": "0",
"9": "9",
A: "A",
B: "B",
y: "y",
z: "z",
"+": "+",
"/": "/",
},
},
});
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
expect(mockNextHandler.mock.calls[0][0].request.headers[USER_AGENT]).toEqual(
expect.stringContaining(`m/0,9,A,B,y,z,+,/`)
);
});
});

describe("should sanitize the SDK user agent string", () => {
const cases: { ua: UserAgentPair; expected: string }[] = [
{ ua: ["/name", "1.0.0"], expected: "name/1.0.0" },
Expand Down
10 changes: 8 additions & 2 deletions packages/middleware-user-agent/src/user-agent-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AwsHandlerExecutionContext } from "@aws-sdk/types";
import { getUserAgentPrefix } from "@aws-sdk/util-endpoints";
import { HttpRequest } from "@smithy/protocol-http";
import {
Expand All @@ -22,6 +23,7 @@ import {
USER_AGENT,
X_AMZ_USER_AGENT,
} from "./constants";
import { encodeMetrics } from "./encode-metrics";

/**
* Build user agent header sections from:
Expand All @@ -39,14 +41,18 @@ export const userAgentMiddleware =
(options: UserAgentResolvedConfig) =>
<Output extends MetadataBearer>(
next: BuildHandler<any, any>,
context: HandlerExecutionContext
context: HandlerExecutionContext | AwsHandlerExecutionContext
): BuildHandler<any, any> =>
async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
const { request } = args;
if (!HttpRequest.isInstance(request)) return next(args);
if (!HttpRequest.isInstance(request)) {
return next(args);
}
const { headers } = request;
const userAgent = context?.userAgent?.map(escapeUserAgent) || [];
const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
const awsContext = context as AwsHandlerExecutionContext;
defaultUserAgent.push(`m/${encodeMetrics(awsContext.__aws_sdk_context?.features ?? {})}`);
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
const prefix = getUserAgentPrefix();

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./encode";
export * from "./endpoint";
export * from "./eventStream";
export * from "./extensions";
export * from "./feature-ids";
export * from "./http";
export * from "./identity";
export * from "./logger";
Expand Down
15 changes: 15 additions & 0 deletions packages/types/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { HandlerExecutionContext } from "@smithy/types";

import { AwsSdkFeatures } from "./feature-ids";

export {
AbsoluteLocation,
BuildHandler,
Expand Down Expand Up @@ -38,3 +42,14 @@ export {
Step,
Terminalware,
} from "@smithy/types";

/**
* @internal
* Contains reserved keys for AWS SDK internal usage of the
* handler execution context object.
*/
export interface AwsHandlerExecutionContext extends HandlerExecutionContext {
__aws_sdk_context?: {
features?: AwsSdkFeatures;
};
}
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.native.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ it("should response basic browser default user agent", async () => {
jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(undefined);
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })();
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
expect(userAgent[1]).toEqual(["ua", "2.0"]);
expect(userAgent[1]).toEqual(["ua", "2.1"]);
expect(userAgent[2]).toEqual(["os/other"]);
expect(userAgent[3]).toEqual(["lang/js"]);
expect(userAgent[4]).toEqual(["md/rn"]);
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const defaultUserAgent =
// sdk-metadata
["aws-sdk-js", clientVersion],
// ua-metadata
["ua", "2.0"],
["ua", "2.1"],
// os-metadata
["os/other"],
// language-metadata
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ it("should populate metrics", async () => {
jest.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
const userAgent = await defaultUserAgent({ serviceId: "s3", clientVersion: "0.1.0" })();
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
expect(userAgent[1]).toEqual(["ua", "2.0"]);
expect(userAgent[1]).toEqual(["ua", "2.1"]);
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
expect(userAgent[3]).toEqual(["lang/js"]);
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const defaultUserAgent =
// sdk-metadata
["aws-sdk-js", clientVersion],
// ua-metadata
["ua", "2.0"],
["ua", "2.1"],
// os-metadata
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
// language-metadata
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-node/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("defaultUserAgent", () => {

const basicUserAgent: UserAgent = [
["aws-sdk-js", "0.1.0"],
["ua", "2.0"],
["ua", "2.1"],
["api/s3", "0.1.0"],
["os/darwin", "19.6.0"],
["lang/js"],
Expand Down
2 changes: 1 addition & 1 deletion packages/util-user-agent-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const defaultUserAgent = ({ serviceId, clientVersion }: DefaultUserAgentO
// sdk-metadata
["aws-sdk-js", clientVersion],
// ua-metadata
["ua", "2.0"],
["ua", "2.1"],
// os-metadata
[`os/${platform()}`, release()],
// language-metadata
Expand Down

0 comments on commit 9dcba0d

Please sign in to comment.