Skip to content

Commit b48810a

Browse files
committed
chore(middleware-sdk-s3): use splitStream utility function to inspect 200-error
1 parent 3758684 commit b48810a

File tree

3 files changed

+67
-24
lines changed

3 files changed

+67
-24
lines changed

packages/middleware-sdk-s3/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"@smithy/smithy-client": "^3.1.8",
3232
"@smithy/types": "^3.3.0",
3333
"@smithy/util-config-provider": "^3.0.0",
34+
"@smithy/util-stream": "^3.0.6",
35+
"@smithy/util-utf8": "^3.0.0",
3436
"tslib": "^2.6.2"
3537
},
3638
"devDependencies": {

packages/middleware-sdk-s3/src/throw-200-exceptions.spec.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
2+
import { toUtf8 } from "@smithy/util-utf8";
3+
import { Readable } from "stream";
24

35
import { throw200ExceptionsMiddleware } from "./throw-200-exceptions";
46

57
describe("throw200ExceptionsMiddlewareOptions", () => {
68
const mockNextHandler = jest.fn();
7-
const mockStreamCollector = jest.fn();
8-
const mockUtf8Encoder = jest.fn();
99
const mockResponse = jest.fn();
1010
const mockConfig = {
11-
streamCollector: mockStreamCollector,
12-
utf8Encoder: mockUtf8Encoder,
11+
utf8Encoder: toUtf8,
1312
};
1413

1514
beforeEach(() => {
1615
jest.clearAllMocks();
1716
});
1817

1918
describe("tests for statusCode < 200 and >= 300", () => {
20-
mockStreamCollector.mockResolvedValue(Buffer.from(""));
21-
mockUtf8Encoder.mockReturnValue("");
22-
2319
it.each([199, 300])("results for statusCode %i", async (statusCode) => {
2420
mockNextHandler.mockReturnValue({
2521
response: mockResponse,
@@ -39,13 +35,11 @@ describe("throw200ExceptionsMiddlewareOptions", () => {
3935

4036
it("should throw if response body is empty", async () => {
4137
expect.assertions(3);
42-
mockStreamCollector.mockResolvedValue(Buffer.from(""));
43-
mockUtf8Encoder.mockReturnValue("");
4438
mockNextHandler.mockReturnValue({
4539
response: new HttpResponse({
4640
statusCode: 200,
4741
headers: {},
48-
body: "",
42+
body: Readable.from(Buffer.from("")),
4943
}),
5044
});
5145
const handler = throw200ExceptionsMiddleware(mockConfig)(mockNextHandler, {
@@ -73,13 +67,11 @@ describe("throw200ExceptionsMiddlewareOptions", () => {
7367
<RequestId>656c76696e6727732072657175657374</RequestId>
7468
<HostId>Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg==</HostId>
7569
</Error>`;
76-
mockStreamCollector.mockResolvedValue(Buffer.from(errorBody));
77-
mockUtf8Encoder.mockReturnValue(errorBody);
7870
mockNextHandler.mockReturnValue({
7971
response: new HttpResponse({
8072
statusCode: 200,
8173
headers: {},
82-
body: "",
74+
body: Readable.from(Buffer.from(errorBody)),
8375
}),
8476
});
8577
const handler = throw200ExceptionsMiddleware(mockConfig)(mockNextHandler, {} as any);
@@ -106,13 +98,40 @@ describe("throw200ExceptionsMiddlewareOptions", () => {
10698
<Message>Access Denied</Message>
10799
</Error>
108100
</DeleteResult>`;
109-
mockStreamCollector.mockResolvedValue(Buffer.from(errorBody));
110-
mockUtf8Encoder.mockReturnValue(errorBody);
111101
mockNextHandler.mockReturnValue({
112102
response: new HttpResponse({
113103
statusCode: 200,
114104
headers: {},
115-
body: "",
105+
body: Readable.from(Buffer.from(errorBody)),
106+
}),
107+
});
108+
const handler = throw200ExceptionsMiddleware(mockConfig)(mockNextHandler, {} as any);
109+
const { response } = await handler({
110+
input: {},
111+
request: new HttpRequest({
112+
hostname: "s3.us-east-1.amazonaws.com",
113+
}),
114+
});
115+
expect(HttpResponse.isInstance(response)).toBe(true);
116+
// @ts-ignore
117+
expect(response.statusCode).toEqual(200);
118+
});
119+
120+
/**
121+
* This is an exception to the specification. We cannot afford to read
122+
* a streaming body for its entire duration just to check for an extremely unlikely
123+
* terminating XML tag if the stream is very long.
124+
*/
125+
it("should not throw if the Error tag is on an excessively long body", async () => {
126+
const errorBody = `<?xml version="1.0" encoding="UTF-8"?>
127+
<Error xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
128+
${"a".repeat(3000)}
129+
</Error>`;
130+
mockNextHandler.mockReturnValue({
131+
response: new HttpResponse({
132+
statusCode: 200,
133+
headers: {},
134+
body: Readable.from(Buffer.from(errorBody)),
116135
}),
117136
});
118137
const handler = throw200ExceptionsMiddleware(mockConfig)(mockNextHandler, {} as any);

packages/middleware-sdk-s3/src/throw-200-exceptions.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
RelativeMiddlewareOptions,
88
StreamCollector,
99
} from "@smithy/types";
10+
import { headStream, splitStream } from "@smithy/util-stream";
1011

1112
type PreviouslyResolved = {
12-
streamCollector: StreamCollector;
1313
utf8Encoder: Encoder;
1414
};
1515

@@ -22,6 +22,13 @@ const THROW_IF_EMPTY_BODY: Record<string, boolean> = {
2222
CompleteMultipartUploadCommand: true,
2323
};
2424

25+
/**
26+
* @internal
27+
* We will check at most this many bytes from the stream when looking for
28+
* an error-like 200 status.
29+
*/
30+
const MAX_BYTES_TO_INSPECT = 3000;
31+
2532
/**
2633
* In case of an internal error/terminated connection, S3 operations may return 200 errors. CopyObject, UploadPartCopy,
2734
* CompleteMultipartUpload may return empty payload or payload with only xml Preamble.
@@ -36,12 +43,25 @@ export const throw200ExceptionsMiddleware =
3643
if (!HttpResponse.isInstance(response)) {
3744
return result;
3845
}
39-
const { statusCode, body } = response;
46+
const { statusCode, body: sourceBody } = response;
4047
if (statusCode < 200 || statusCode >= 300) {
4148
return result;
4249
}
4350

44-
const bodyBytes: Uint8Array = await collectBody(body, config);
51+
let bodyCopy = sourceBody;
52+
let body = sourceBody;
53+
54+
if (sourceBody && typeof sourceBody === "object" && !(sourceBody instanceof Uint8Array)) {
55+
[bodyCopy, body] = await splitStream(sourceBody);
56+
}
57+
// restore split body to the response for deserialization.
58+
response.body = body;
59+
60+
const bodyBytes: Uint8Array = await collectBody(bodyCopy, {
61+
streamCollector: async (stream: any) => {
62+
return headStream(stream, MAX_BYTES_TO_INSPECT);
63+
},
64+
});
4565
const bodyStringTail = config.utf8Encoder(bodyBytes.subarray(bodyBytes.length - 16));
4666

4767
// Throw on 200 response with empty body, legacy behavior allowlist.
@@ -56,14 +76,16 @@ export const throw200ExceptionsMiddleware =
5676
response.statusCode = 400;
5777
}
5878

59-
// Body stream is consumed and paused at this point. So replace the response.body to the collected bytes.
60-
// So that the deserializer can consume the body as normal.
61-
response.body = bodyBytes;
6279
return result;
6380
};
6481

65-
// Collect low-level response body stream to Uint8Array.
66-
const collectBody = (streamBody: any = new Uint8Array(), context: PreviouslyResolved): Promise<Uint8Array> => {
82+
/**
83+
* @internal
84+
*/
85+
const collectBody = (
86+
streamBody: any = new Uint8Array(),
87+
context: { streamCollector: StreamCollector }
88+
): Promise<Uint8Array> => {
6789
if (streamBody instanceof Uint8Array) {
6890
return Promise.resolve(streamBody);
6991
}

0 commit comments

Comments
 (0)