Skip to content

Commit c0ed833

Browse files
authored
fix(middleware-sdk-s3): improve error message on stream upload (#3571)
* fix(middleware-sdk-s3): improve error message on stream upload * feat(middleware-sdk-s3): add warning when calling PutObject with unknown content length * fix(middleware-sdk-s3): correct middleware name
1 parent 03e589f commit c0ed833

File tree

6 files changed

+161
-0
lines changed

6 files changed

+161
-0
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ codegen/sdk-codegen/smithy-build.json
3636
.gradle
3737
*/out/
3838
*/*/out/
39+
workspace
3940

4041
coverage
4142
dist

Diff for: clients/client-s3/src/commands/PutObjectCommand.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// smithy-typescript generated code
22
import { getBucketEndpointPlugin } from "@aws-sdk/middleware-bucket-endpoint";
33
import { getFlexibleChecksumsPlugin } from "@aws-sdk/middleware-flexible-checksums";
4+
import { getCheckContentLengthHeaderPlugin } from "@aws-sdk/middleware-sdk-s3";
45
import { getSerdePlugin } from "@aws-sdk/middleware-serde";
56
import { getSsecPlugin } from "@aws-sdk/middleware-ssec";
67
import { HttpRequest as __HttpRequest, HttpResponse as __HttpResponse } from "@aws-sdk/protocol-http";
@@ -173,6 +174,7 @@ export class PutObjectCommand extends $Command<PutObjectCommandInput, PutObjectC
173174
options?: __HttpHandlerOptions
174175
): Handler<PutObjectCommandInput, PutObjectCommandOutput> {
175176
this.middlewareStack.use(getSerdePlugin(configuration, this.serialize, this.deserialize));
177+
this.middlewareStack.use(getCheckContentLengthHeaderPlugin(configuration));
176178
this.middlewareStack.use(getSsecPlugin(configuration));
177179
this.middlewareStack.use(getBucketEndpointPlugin(configuration));
178180
this.middlewareStack.use(

Diff for: codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddS3Config.java

+5
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ public List<RuntimeClientPlugin> getClientPlugins() {
162162
HAS_MIDDLEWARE)
163163
.servicePredicate((m, s) -> testServiceId(s))
164164
.build(),
165+
RuntimeClientPlugin.builder()
166+
.withConventions(AwsDependency.S3_MIDDLEWARE.dependency, "CheckContentLengthHeader",
167+
HAS_MIDDLEWARE)
168+
.operationPredicate((m, s, o) -> testServiceId(s) && o.getId().getName(s).equals("PutObject"))
169+
.build(),
165170
RuntimeClientPlugin.builder()
166171
.withConventions(AwsDependency.S3_MIDDLEWARE.dependency, "UseRegionalEndpoint",
167172
HAS_MIDDLEWARE)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { checkContentLengthHeader } from "./check-content-length-header";
2+
3+
describe("checkContentLengthHeaderMiddleware", () => {
4+
const mockNextHandler = jest.fn();
5+
6+
let spy;
7+
8+
beforeEach(() => {
9+
spy = jest.spyOn(console, "warn");
10+
});
11+
12+
afterEach(() => {
13+
jest.clearAllMocks();
14+
});
15+
16+
it("warns if uploading a payload of unknown length", async () => {
17+
const handler = checkContentLengthHeader()(mockNextHandler, {});
18+
19+
await handler({
20+
request: {
21+
method: null,
22+
protocol: null,
23+
hostname: null,
24+
path: null,
25+
query: {},
26+
headers: {},
27+
},
28+
input: {},
29+
});
30+
31+
expect(spy).toHaveBeenCalledWith(
32+
"Are you using a Stream of unknown length as the Body of a PutObject request? Consider using Upload instead from @aws-sdk/lib-storage."
33+
);
34+
});
35+
36+
it("uses the context logger if available", async () => {
37+
const context = {
38+
logger: {
39+
called: false,
40+
calledWith: "",
41+
warn(msg) {
42+
this.called = true;
43+
this.calledWith = msg;
44+
},
45+
debug() {},
46+
info() {},
47+
error() {},
48+
},
49+
};
50+
const handler = checkContentLengthHeader()(mockNextHandler, context);
51+
52+
await handler({
53+
request: {
54+
method: null,
55+
protocol: null,
56+
hostname: null,
57+
path: null,
58+
query: {},
59+
headers: {},
60+
},
61+
input: {},
62+
});
63+
64+
expect(spy).not.toHaveBeenCalled();
65+
expect(context.logger.called).toBe(true);
66+
expect(context.logger.calledWith).toEqual(
67+
"Are you using a Stream of unknown length as the Body of a PutObject request? Consider using Upload instead from @aws-sdk/lib-storage."
68+
);
69+
});
70+
71+
it("does not warn if uploading a payload of known length", async () => {
72+
const handler = checkContentLengthHeader()(mockNextHandler, {});
73+
74+
await handler({
75+
request: {
76+
method: null,
77+
protocol: null,
78+
hostname: null,
79+
path: null,
80+
query: {},
81+
headers: {
82+
"content-length": "5",
83+
},
84+
},
85+
input: {},
86+
});
87+
88+
expect(spy).not.toHaveBeenCalled();
89+
});
90+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { HttpRequest } from "@aws-sdk/protocol-http";
2+
import {
3+
FinalizeHandler,
4+
FinalizeHandlerArguments,
5+
FinalizeHandlerOutput,
6+
FinalizeRequestHandlerOptions,
7+
FinalizeRequestMiddleware,
8+
HandlerExecutionContext,
9+
MetadataBearer,
10+
Pluggable,
11+
} from "@aws-sdk/types";
12+
13+
const CONTENT_LENGTH_HEADER = "content-length";
14+
15+
/**
16+
* @internal
17+
*
18+
* Log a warning if the input to PutObject is detected to be a Stream of unknown ContentLength and
19+
* recommend the usage of the @aws-sdk/lib-storage Upload class.
20+
*/
21+
export function checkContentLengthHeader(): FinalizeRequestMiddleware<any, any> {
22+
return <Output extends MetadataBearer>(
23+
next: FinalizeHandler<any, Output>,
24+
context: HandlerExecutionContext
25+
): FinalizeHandler<any, Output> =>
26+
async (args: FinalizeHandlerArguments<any>): Promise<FinalizeHandlerOutput<Output>> => {
27+
const { request } = args;
28+
29+
if (HttpRequest.isInstance(request)) {
30+
if (!request.headers[CONTENT_LENGTH_HEADER]) {
31+
const message = `Are you using a Stream of unknown length as the Body of a PutObject request? Consider using Upload instead from @aws-sdk/lib-storage.`;
32+
if (typeof context?.logger?.warn === "function") {
33+
context.logger.warn(message);
34+
} else {
35+
console.warn(message);
36+
}
37+
}
38+
}
39+
40+
return next({ ...args });
41+
};
42+
}
43+
44+
/**
45+
* @internal
46+
*/
47+
export const checkContentLengthHeaderMiddlewareOptions: FinalizeRequestHandlerOptions = {
48+
step: "finalizeRequest",
49+
tags: ["CHECK_CONTENT_LENGTH_HEADER"],
50+
name: "getCheckContentLengthHeaderPlugin",
51+
override: true,
52+
};
53+
54+
/**
55+
* @internal
56+
*/
57+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
58+
export const getCheckContentLengthHeaderPlugin = (unused: any): Pluggable<any, any> => ({
59+
applyToStack: (clientStack) => {
60+
clientStack.add(checkContentLengthHeader(), checkContentLengthHeaderMiddlewareOptions);
61+
},
62+
});

Diff for: packages/middleware-sdk-s3/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./check-content-length-header";
12
export * from "./throw-200-exceptions";
23
export * from "./use-regional-endpoint";
34
export * from "./validate-bucket-name";

0 commit comments

Comments
 (0)