Skip to content

Commit f05b540

Browse files
authored
fix(middleware-sdk-s3): restore bucketEndpoint configurability (#5840)
* fix(middleware-sdk-s3): restore bucketEndpoint configurability * test: fix tests
1 parent 1e2b1ae commit f05b540

File tree

6 files changed

+100
-8
lines changed

6 files changed

+100
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
HandlerExecutionContext,
3+
MetadataBearer,
4+
RelativeMiddlewareOptions,
5+
SerializeHandler,
6+
SerializeHandlerArguments,
7+
SerializeHandlerOutput,
8+
SerializeMiddleware,
9+
} from "@smithy/types";
10+
11+
interface PreviouslyResolved {
12+
bucketEndpoint?: boolean;
13+
}
14+
15+
/**
16+
* @internal
17+
*/
18+
export function bucketEndpointMiddleware(options: PreviouslyResolved): SerializeMiddleware<any, any> {
19+
return <Output extends MetadataBearer>(
20+
next: SerializeHandler<any, Output>,
21+
context: HandlerExecutionContext
22+
): SerializeHandler<any, Output> =>
23+
async (args: SerializeHandlerArguments<any>): Promise<SerializeHandlerOutput<Output>> => {
24+
if (options.bucketEndpoint) {
25+
const endpoint = context.endpointV2;
26+
if (endpoint) {
27+
const bucket: string | undefined = args.input.Bucket;
28+
if (typeof bucket === "string") {
29+
try {
30+
const bucketEndpointUrl = new URL(bucket);
31+
endpoint.url = bucketEndpointUrl;
32+
} catch (e) {
33+
const warning = `@aws-sdk/middleware-sdk-s3: bucketEndpoint=true was set but Bucket=${bucket} could not be parsed as URL.`;
34+
if (context.logger?.constructor?.name === "NoOpLogger") {
35+
console.warn(warning);
36+
} else {
37+
context.logger?.warn?.(warning);
38+
}
39+
throw e;
40+
}
41+
}
42+
}
43+
}
44+
return next(args);
45+
};
46+
}
47+
48+
/**
49+
* @internal
50+
*/
51+
export const bucketEndpointMiddlewareOptions: RelativeMiddlewareOptions = {
52+
name: "bucketEndpointMiddleware",
53+
override: true,
54+
relation: "after",
55+
toMiddleware: "endpointV2Middleware",
56+
};

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

+26
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,31 @@ describe("middleware-sdk-s3", () => {
5757

5858
expect.hasAssertions();
5959
});
60+
61+
it("allows using a bucket input value as the endpoint", async () => {
62+
const client = new S3({
63+
region: "us-west-2",
64+
bucketEndpoint: true,
65+
});
66+
67+
requireRequestsFrom(client).toMatch({
68+
query: { "x-id": "PutObject" },
69+
protocol: "https:",
70+
hostname: "mybucket.com",
71+
port: 8888,
72+
path: "/my-bucket-path/my-key",
73+
headers: {
74+
host: "mybucket.com:8888",
75+
},
76+
});
77+
78+
await client.putObject({
79+
Bucket: "https://mybucket.com:8888/my-bucket-path",
80+
Key: "my-key",
81+
Body: "abcd",
82+
});
83+
84+
expect.hasAssertions();
85+
});
6086
});
6187
});

packages/middleware-sdk-s3/src/s3Configuration.ts

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export interface S3InputConfig {
3232
* Identity provider for an S3 feature.
3333
*/
3434
s3ExpressIdentityProvider?: S3ExpressIdentityProvider;
35+
/**
36+
* Whether to use the bucket name as the endpoint for this client.
37+
*/
38+
bucketEndpoint?: boolean;
3539
}
3640

3741
/**
@@ -54,6 +58,7 @@ export interface S3ResolvedConfig {
5458
disableMultiregionAccessPoints: boolean;
5559
followRegionRedirects: boolean;
5660
s3ExpressIdentityProvider: S3ExpressIdentityProvider;
61+
bucketEndpoint: boolean;
5762
}
5863

5964
export const resolveS3Config = <T>(
@@ -81,5 +86,6 @@ export const resolveS3Config = <T>(
8186
})
8287
)
8388
),
89+
bucketEndpoint: input.bucketEndpoint ?? false,
8490
};
8591
};

packages/middleware-sdk-s3/src/validate-bucket-name.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe("validateBucketNameMiddleware", () => {
1111
});
1212

1313
it("throws error if Bucket parameter contains '/'", async () => {
14-
const handler = validateBucketNameMiddleware()(mockNextHandler, {} as any);
14+
const handler = validateBucketNameMiddleware({} as any)(mockNextHandler, {} as any);
1515
const bucket = "bucket/part/of/key";
1616
let error;
1717
try {
@@ -29,7 +29,7 @@ describe("validateBucketNameMiddleware", () => {
2929
});
3030

3131
it("doesn't throw error if Bucket parameter has no '/'", async () => {
32-
const handler = validateBucketNameMiddleware()(mockNextHandler, {} as any);
32+
const handler = validateBucketNameMiddleware({} as any)(mockNextHandler, {} as any);
3333
const args = {
3434
input: {
3535
Bucket: "bucket",
@@ -42,7 +42,7 @@ describe("validateBucketNameMiddleware", () => {
4242

4343
it("should not validate bucket name if the bucket name is an ARN", async () => {
4444
mockValidateArn.mockReturnValue(true);
45-
const handler = validateBucketNameMiddleware()(mockNextHandler, {} as any);
45+
const handler = validateBucketNameMiddleware({} as any)(mockNextHandler, {} as any);
4646
const args = {
4747
input: {
4848
Bucket: "arn:aws:s3:us-east-1:123456789012:accesspoint/myendpoint",

packages/middleware-sdk-s3/src/validate-bucket-name.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ import {
99
Pluggable,
1010
} from "@smithy/types";
1111

12+
import { bucketEndpointMiddleware, bucketEndpointMiddlewareOptions } from "./bucket-endpoint-middleware";
13+
import { S3ResolvedConfig } from "./s3Configuration";
14+
1215
/**
1316
* @internal
1417
*/
15-
export function validateBucketNameMiddleware(): InitializeMiddleware<any, any> {
18+
export function validateBucketNameMiddleware({ bucketEndpoint }: S3ResolvedConfig): InitializeMiddleware<any, any> {
1619
return <Output extends MetadataBearer>(next: InitializeHandler<any, Output>): InitializeHandler<any, Output> =>
1720
async (args: InitializeHandlerArguments<any>): Promise<InitializeHandlerOutput<Output>> => {
1821
const {
1922
input: { Bucket },
2023
} = args;
21-
if (typeof Bucket === "string" && !validateArn(Bucket) && Bucket.indexOf("/") >= 0) {
24+
if (!bucketEndpoint && typeof Bucket === "string" && !validateArn(Bucket) && Bucket.indexOf("/") >= 0) {
2225
const err = new Error(`Bucket name shouldn't contain '/', received '${Bucket}'`);
2326
err.name = "InvalidBucketName";
2427
throw err;
@@ -40,9 +43,9 @@ export const validateBucketNameMiddlewareOptions: InitializeHandlerOptions = {
4043
/**
4144
* @internal
4245
*/
43-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
44-
export const getValidateBucketNamePlugin = (unused: any): Pluggable<any, any> => ({
46+
export const getValidateBucketNamePlugin = (options: S3ResolvedConfig): Pluggable<any, any> => ({
4547
applyToStack: (clientStack) => {
46-
clientStack.add(validateBucketNameMiddleware(), validateBucketNameMiddlewareOptions);
48+
clientStack.add(validateBucketNameMiddleware(options), validateBucketNameMiddlewareOptions);
49+
clientStack.addRelativeTo(bucketEndpointMiddleware(options), bucketEndpointMiddlewareOptions);
4750
},
4851
});

private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export const initializeWithMaximalConfiguration = () => {
116116
disableS3ExpressSessionAuth: false,
117117
useGlobalEndpoint: false,
118118
signingEscapePath: false,
119+
bucketEndpoint: false,
119120
};
120121

121122
const s3 = new S3Client(config);

0 commit comments

Comments
 (0)