Skip to content

Commit 2da1b6c

Browse files
siddsrivkuhe
andauthored
fix(middleware-sdk-sqs): add middleware to prioritize QueueUrl endpoint resolution (#5759)
* fix(middleware-sdk-sqs): add middleware to prioritize QueueUrl endpoint resolution * fix(middleware-sdk-sqs): add option to use QueueUrl as endpoint * test(middleware-sdk-sqs): initial unit test for queue-url middleware * test(middleware-sdk-sqs): fix unit and add integ tests * test(middleware-sdk-sqs): avoid using QueueUrl if custom endpoint given to client * test(middleware-sdk-sqs): warn with exact error --------- Co-authored-by: George Fu <[email protected]>
1 parent 4d6db90 commit 2da1b6c

File tree

7 files changed

+276
-5
lines changed

7 files changed

+276
-5
lines changed

clients/client-sqs/src/SQSClient.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import {
77
} from "@aws-sdk/middleware-host-header";
88
import { getLoggerPlugin } from "@aws-sdk/middleware-logger";
99
import { getRecursionDetectionPlugin } from "@aws-sdk/middleware-recursion-detection";
10+
import {
11+
getQueueUrlPlugin,
12+
QueueUrlInputConfig,
13+
QueueUrlResolvedConfig,
14+
resolveQueueUrlConfig,
15+
} from "@aws-sdk/middleware-sdk-sqs";
1016
import {
1117
getUserAgentPlugin,
1218
resolveUserAgentConfig,
@@ -311,6 +317,7 @@ export type SQSClientConfigType = Partial<__SmithyConfiguration<__HttpHandlerOpt
311317
EndpointInputConfig<EndpointParameters> &
312318
RetryInputConfig &
313319
HostHeaderInputConfig &
320+
QueueUrlInputConfig &
314321
UserAgentInputConfig &
315322
HttpAuthSchemeInputConfig &
316323
ClientInputEndpointParameters;
@@ -331,6 +338,7 @@ export type SQSClientResolvedConfigType = __SmithyResolvedConfiguration<__HttpHa
331338
EndpointResolvedConfig<EndpointParameters> &
332339
RetryResolvedConfig &
333340
HostHeaderResolvedConfig &
341+
QueueUrlResolvedConfig &
334342
UserAgentResolvedConfig &
335343
HttpAuthSchemeResolvedConfig &
336344
ClientResolvedEndpointParameters;
@@ -435,16 +443,18 @@ export class SQSClient extends __Client<
435443
const _config_3 = resolveEndpointConfig(_config_2);
436444
const _config_4 = resolveRetryConfig(_config_3);
437445
const _config_5 = resolveHostHeaderConfig(_config_4);
438-
const _config_6 = resolveUserAgentConfig(_config_5);
439-
const _config_7 = resolveHttpAuthSchemeConfig(_config_6);
440-
const _config_8 = resolveRuntimeExtensions(_config_7, configuration?.extensions || []);
441-
super(_config_8);
442-
this.config = _config_8;
446+
const _config_6 = resolveQueueUrlConfig(_config_5);
447+
const _config_7 = resolveUserAgentConfig(_config_6);
448+
const _config_8 = resolveHttpAuthSchemeConfig(_config_7);
449+
const _config_9 = resolveRuntimeExtensions(_config_8, configuration?.extensions || []);
450+
super(_config_9);
451+
this.config = _config_9;
443452
this.middlewareStack.use(getRetryPlugin(this.config));
444453
this.middlewareStack.use(getContentLengthPlugin(this.config));
445454
this.middlewareStack.use(getHostHeaderPlugin(this.config));
446455
this.middlewareStack.use(getLoggerPlugin(this.config));
447456
this.middlewareStack.use(getRecursionDetectionPlugin(this.config));
457+
this.middlewareStack.use(getQueueUrlPlugin(this.config));
448458
this.middlewareStack.use(getUserAgentPlugin(this.config));
449459
this.middlewareStack.use(
450460
getHttpAuthSchemeEndpointRuleSetPlugin(this.config, {

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddSqsDependency.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.smithy.aws.typescript.codegen;
1717

18+
import static software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin.Convention.HAS_CONFIG;
1819
import static software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin.Convention.HAS_MIDDLEWARE;
1920

2021
import java.util.Collections;
@@ -58,6 +59,14 @@ public List<RuntimeClientPlugin> getClientPlugins() {
5859
.withConventions(AwsDependency.SQS_MIDDLEWARE.dependency, "ReceiveMessage",
5960
HAS_MIDDLEWARE)
6061
.operationPredicate((m, s, o) -> o.getId().getName(s).equals("ReceiveMessage") && isSqs(s))
62+
.build(),
63+
RuntimeClientPlugin.builder()
64+
.withConventions(
65+
AwsDependency.SQS_MIDDLEWARE.dependency,
66+
"QueueUrl",
67+
HAS_MIDDLEWARE, HAS_CONFIG
68+
)
69+
.servicePredicate((m, s) -> isSqs(s))
6170
.build()
6271
);
6372
}

packages/middleware-sdk-sqs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"license": "Apache-2.0",
2323
"dependencies": {
2424
"@aws-sdk/types": "*",
25+
"@smithy/smithy-client": "^2.3.1",
2526
"@smithy/types": "^2.9.1",
2627
"@smithy/util-hex-encoding": "^2.1.1",
2728
"@smithy/util-utf8": "^2.1.1",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./queue-url";
12
export * from "./receive-message";
23
export * from "./send-message";
34
export * from "./send-message-batch";

packages/middleware-sdk-sqs/src/middleware-sdk-sqs.integ.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import crypto from "crypto";
44
import { Readable } from "stream";
55

66
import sqsModel from "../../../codegen/sdk-codegen/aws-models/sqs.json";
7+
import { requireRequestsFrom } from "../../../private/aws-util-test/src";
78
const useAwsQuery = !!sqsModel.shapes["com.amazonaws.sqs#AmazonSQS"].traits["aws.protocols#awsQuery"];
89

910
let hashError = "";
@@ -289,4 +290,59 @@ describe("middleware-sdk-sqs", () => {
289290
});
290291
});
291292
});
293+
294+
describe("queue-url", () => {
295+
it("should override resolved endpoint by default", async () => {
296+
const client = new SQS({
297+
region: "us-west-2",
298+
});
299+
300+
requireRequestsFrom(client).toMatch({
301+
hostname: "abc.com",
302+
protocol: "https:",
303+
path: "/",
304+
});
305+
306+
await client.sendMessage({
307+
QueueUrl: "https://abc.com/123/MyQueue",
308+
MessageBody: "hello",
309+
});
310+
});
311+
312+
it("does not override endpoint if shut off with useQueueUrlAsEndpoint=false", async () => {
313+
const client = new SQS({
314+
region: "us-west-2",
315+
useQueueUrlAsEndpoint: false,
316+
});
317+
318+
requireRequestsFrom(client).toMatch({
319+
hostname: "sqs.us-west-2.amazonaws.com",
320+
protocol: "https:",
321+
path: "/",
322+
});
323+
324+
await client.sendMessage({
325+
QueueUrl: "https://abc.com/123/MyQueue",
326+
MessageBody: "hello",
327+
});
328+
});
329+
330+
it("does not override endpoint if custom endpoint given to client", async () => {
331+
const client = new SQS({
332+
region: "us-west-2",
333+
endpoint: "https://custom-endpoint.com/",
334+
});
335+
336+
requireRequestsFrom(client).toMatch({
337+
hostname: "custom-endpoint.com",
338+
protocol: "https:",
339+
path: "/",
340+
});
341+
342+
await client.sendMessage({
343+
QueueUrl: "https://abc.com/123/MyQueue",
344+
MessageBody: "hello",
345+
});
346+
});
347+
});
292348
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { HttpRequest } from "@aws-sdk/protocol-http";
2+
import { FinalizeHandlerArguments, HandlerExecutionContext } from "@aws-sdk/types";
3+
4+
import { queueUrlMiddleware } from "./queue-url";
5+
6+
describe("queueUrlMiddleware", () => {
7+
const mockNextHandler = jest.fn();
8+
9+
const mockContext: HandlerExecutionContext = {
10+
logger: {
11+
...console,
12+
warn: jest.fn(),
13+
},
14+
endpointV2: void 0,
15+
};
16+
17+
beforeEach(() => {
18+
mockNextHandler.mockReset();
19+
mockNextHandler.mockResolvedValue({ output: {}, response: {} });
20+
mockContext.endpointV2 = {
21+
url: new URL("https://sqs.us-east-1.amazonaws.com"),
22+
};
23+
});
24+
25+
afterEach(() => {
26+
jest.resetAllMocks();
27+
});
28+
29+
it("should use the QueueUrl hostname as the endpoint if useQueueUrlAsEndpoint is true", async () => {
30+
const middleware = queueUrlMiddleware({ useQueueUrlAsEndpoint: true });
31+
const input = { QueueUrl: "https://xyz.com/123/MyQueue" };
32+
const request = new HttpRequest({
33+
hostname: "sqs.us-east-1.amazonaws.com",
34+
protocol: "https:",
35+
path: "/",
36+
headers: {},
37+
method: "GET",
38+
});
39+
const args: FinalizeHandlerArguments<any> = { input, request };
40+
41+
await middleware(mockNextHandler, mockContext)(args);
42+
43+
// Verify that the resolvedEndpoint.url has been modified to match QueueUrl
44+
expect(mockContext.endpointV2?.url.href).toEqual("https://xyz.com/");
45+
expect(mockNextHandler).toHaveBeenCalled();
46+
expect(mockContext.logger?.warn).toHaveBeenCalled();
47+
});
48+
49+
it("should not modify the endpoint if useQueueUrlAsEndpoint is false", async () => {
50+
const middleware = queueUrlMiddleware({ useQueueUrlAsEndpoint: false });
51+
const input = { QueueUrl: "https://xyz.com/123/MyQueue" };
52+
const request = new HttpRequest({
53+
hostname: "sqs.us-east-1.amazonaws.com",
54+
protocol: "https:",
55+
path: "/",
56+
});
57+
const args: FinalizeHandlerArguments<any> = { input, request };
58+
59+
await middleware(mockNextHandler, mockContext)(args);
60+
61+
expect(mockNextHandler).toHaveBeenCalledWith(args);
62+
expect(mockContext.endpointV2?.url.href).toEqual("https://sqs.us-east-1.amazonaws.com/");
63+
});
64+
65+
it("should not modify the endpoint when QueueUrl is not provided", async () => {
66+
const middleware = queueUrlMiddleware({ useQueueUrlAsEndpoint: true });
67+
const input = {}; // No QueueUrl provided
68+
const request = new HttpRequest({
69+
hostname: "sqs.us-east-1.amazonaws.com",
70+
protocol: "https:",
71+
path: "/",
72+
});
73+
const args: FinalizeHandlerArguments<any> = { input, request };
74+
75+
await middleware(mockNextHandler, mockContext)(args);
76+
77+
expect(mockNextHandler).toHaveBeenCalledWith(args);
78+
expect(mockContext.endpointV2?.url.href).toEqual("https://sqs.us-east-1.amazonaws.com/");
79+
});
80+
81+
it("should not modify the endpoint when a custom endpoint is provided in config", async () => {
82+
const middleware = queueUrlMiddleware({ useQueueUrlAsEndpoint: true, endpoint: "https://my-endpoint.com/" });
83+
const input = { QueueUrl: "https://xyz.com/123/MyQueue" };
84+
const request = new HttpRequest({
85+
hostname: "my-endpoint.com",
86+
protocol: "https:",
87+
path: "/",
88+
});
89+
const args: FinalizeHandlerArguments<any> = { input, request };
90+
91+
await middleware(mockNextHandler, mockContext)(args);
92+
93+
expect(mockNextHandler).toHaveBeenCalledWith(args);
94+
expect(mockContext.endpointV2?.url.href).toEqual("https://sqs.us-east-1.amazonaws.com/");
95+
expect(mockContext.logger?.warn).not.toHaveBeenCalled();
96+
});
97+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
Endpoint,
3+
EndpointV2,
4+
FinalizeHandlerArguments,
5+
FinalizeHandlerOutput,
6+
HandlerExecutionContext,
7+
MiddlewareStack,
8+
Pluggable,
9+
Provider,
10+
RelativeMiddlewareOptions,
11+
} from "@aws-sdk/types";
12+
import { NoOpLogger } from "@smithy/smithy-client";
13+
14+
/**
15+
* @public
16+
*/
17+
export interface QueueUrlInputConfig {
18+
/**
19+
* In cases where a QueueUrl is given as input, that
20+
* will be preferred as the request endpoint.
21+
*
22+
* Set this value to false to ignore the QueueUrl and use the
23+
* client's resolved endpoint, which may be a custom endpoint.
24+
*/
25+
useQueueUrlAsEndpoint?: boolean;
26+
}
27+
28+
export interface QueueUrlResolvedConfig {
29+
useQueueUrlAsEndpoint: boolean;
30+
}
31+
32+
export interface PreviouslyResolved {
33+
endpoint?: string | Endpoint | Provider<Endpoint> | EndpointV2 | Provider<EndpointV2>;
34+
}
35+
36+
export const resolveQueueUrlConfig = <T>(
37+
config: T & PreviouslyResolved & QueueUrlInputConfig
38+
): T & QueueUrlResolvedConfig => {
39+
return {
40+
...config,
41+
useQueueUrlAsEndpoint: config.useQueueUrlAsEndpoint ?? true,
42+
};
43+
};
44+
45+
/**
46+
* @internal
47+
*/
48+
export function queueUrlMiddleware({ useQueueUrlAsEndpoint, endpoint }: QueueUrlResolvedConfig & PreviouslyResolved) {
49+
return <Output extends object>(
50+
next: (args: FinalizeHandlerArguments<any>) => Promise<FinalizeHandlerOutput<Output>>,
51+
context: HandlerExecutionContext
52+
): ((args: FinalizeHandlerArguments<any>) => Promise<FinalizeHandlerOutput<Output>>) => {
53+
return async (args: FinalizeHandlerArguments<any>): Promise<FinalizeHandlerOutput<Output>> => {
54+
const { input } = args;
55+
const resolvedEndpoint = context.endpointV2;
56+
57+
if (!endpoint && input.QueueUrl && resolvedEndpoint && useQueueUrlAsEndpoint) {
58+
const logger = context.logger instanceof NoOpLogger || !context.logger?.warn ? console : context.logger;
59+
try {
60+
const queueUrl: URL = new URL(input.QueueUrl);
61+
const queueUrlOrigin: URL = new URL(queueUrl.origin);
62+
if (resolvedEndpoint.url.origin !== queueUrlOrigin.origin) {
63+
logger.warn(
64+
`QueueUrl=${
65+
input.QueueUrl
66+
} differs from SQSClient resolved endpoint=${resolvedEndpoint.url.toString()}, using QueueUrl host as endpoint.
67+
Set [endpoint=string] or [useQueueUrlAsEndpoint=false] on the SQSClient.`
68+
);
69+
resolvedEndpoint.url = queueUrlOrigin;
70+
}
71+
} catch (e: unknown) {
72+
logger.warn(e);
73+
}
74+
}
75+
return next(args);
76+
};
77+
};
78+
}
79+
80+
/**
81+
* @internal
82+
*/
83+
export const queueUrlMiddlewareOptions: RelativeMiddlewareOptions = {
84+
name: "queueUrlMiddleware",
85+
relation: "after",
86+
toMiddleware: "endpointV2Middleware",
87+
override: true,
88+
};
89+
90+
/**
91+
* @internal
92+
*/
93+
export const getQueueUrlPlugin = (config: QueueUrlResolvedConfig): Pluggable<any, any> => ({
94+
applyToStack: (clientStack: MiddlewareStack<any, any>) => {
95+
clientStack.addRelativeTo(queueUrlMiddleware(config), queueUrlMiddlewareOptions);
96+
},
97+
});

0 commit comments

Comments
 (0)