Skip to content

Commit 3500f34

Browse files
kuhetrivikr
andauthored
feat: improve support for fetch and web-streams in Node.js (#1256)
* feat: improve fetch-http-handler compatibility in Node.js * add changeset * unit tests * formatting * Update packages/fetch-http-handler/README.md Co-authored-by: Trivikram Kamat <[email protected]> * Update packages/node-http-handler/src/stream-collector/index.ts Co-authored-by: Trivikram Kamat <[email protected]> * test: modify conditional unit tests --------- Co-authored-by: Trivikram Kamat <[email protected]>
1 parent 671aa70 commit 3500f34

File tree

8 files changed

+105
-17
lines changed

8 files changed

+105
-17
lines changed

.changeset/happy-monkeys-tap.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/node-http-handler": minor
3+
"@smithy/util-stream": minor
4+
---
5+
6+
handle web streams in streamCollector and sdkStreamMixin

packages/fetch-http-handler/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@
22

33
[![NPM version](https://img.shields.io/npm/v/@smithy/fetch-http-handler/latest.svg)](https://www.npmjs.com/package/@smithy/fetch-http-handler)
44
[![NPM downloads](https://img.shields.io/npm/dm/@smithy/fetch-http-handler.svg)](https://www.npmjs.com/package/@smithy/fetch-http-handler)
5+
6+
This is the default `requestHandler` used for browser applications.
7+
Since Node.js introduced experimental Web Streams API in v16.5.0 and made it stable in v21.0.0,
8+
you can consider using `fetch-http-handler` in Node.js, although it's not recommended.
9+
10+
For the Node.js default `requestHandler` implementation, see instead
11+
[`@smithy/node-http-handler`](https://www.npmjs.com/package/@smithy/node-http-handler).

packages/node-http-handler/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22

33
[![NPM version](https://img.shields.io/npm/v/@smithy/node-http-handler/latest.svg)](https://www.npmjs.com/package/@smithy/node-http-handler)
44
[![NPM downloads](https://img.shields.io/npm/dm/@smithy/node-http-handler.svg)](https://www.npmjs.com/package/@smithy/node-http-handler)
5+
6+
This package implements the default `requestHandler` for Node.js using `node:http`, `node:https`, and `node:http2`.
7+
8+
For an example on how `requestHandler`s are used by Smithy generated SDK clients, refer to
9+
the [AWS SDK for JavaScript (v3) supplemental docs](https://github.com/aws/aws-sdk-js-v3/blob/main/supplemental-docs/CLIENTS.md#request-handler-requesthandler).

packages/node-http-handler/src/stream-collector/index.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ describe("streamCollector", () => {
1212
expect(collectedData).toEqual(expected);
1313
});
1414

15+
it("accepts ReadableStream if the global web stream implementation exists in Node.js", async () => {
16+
if (typeof ReadableStream === "function") {
17+
const data = await streamCollector(
18+
new ReadableStream({
19+
start(controller) {
20+
controller.enqueue(Buffer.from("abcd"));
21+
controller.close();
22+
},
23+
})
24+
);
25+
expect(Buffer.from(data)).toEqual(Buffer.from("abcd"));
26+
}
27+
});
28+
1529
it("will propagate errors from the stream", async () => {
1630
// stream should emit an error right away
1731
const mockReadStream = new ReadFromBuffers({

packages/node-http-handler/src/stream-collector/index.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { StreamCollector } from "@smithy/types";
22
import { Readable } from "stream";
3+
import type { ReadableStream as IReadableStream } from "stream/web";
34

45
import { Collector } from "./collector";
56

6-
export const streamCollector: StreamCollector = (stream: Readable): Promise<Uint8Array> =>
7-
new Promise((resolve, reject) => {
7+
export const streamCollector: StreamCollector = (stream: Readable | IReadableStream): Promise<Uint8Array> => {
8+
if (isReadableStreamInstance(stream)) {
9+
// Web stream API in Node.js
10+
return collectReadableStream(stream);
11+
}
12+
return new Promise((resolve, reject) => {
813
const collector = new Collector();
914
stream.pipe(collector);
1015
stream.on("error", (err) => {
@@ -18,3 +23,30 @@ export const streamCollector: StreamCollector = (stream: Readable): Promise<Uint
1823
resolve(bytes);
1924
});
2025
});
26+
};
27+
28+
/**
29+
* Note: the global.ReadableStream object is marked experimental, and was added in v18.0.0 of Node.js.
30+
* The importable version was added in v16.5.0. We only test for the global version so as not to
31+
* enforce an import on a Node.js version that may not have it, and import
32+
* only the type from stream/web.
33+
*/
34+
const isReadableStreamInstance = (stream: unknown): stream is IReadableStream =>
35+
typeof ReadableStream === "function" && stream instanceof ReadableStream;
36+
37+
async function collectReadableStream(stream: IReadableStream): Promise<Uint8Array> {
38+
let res = new Uint8Array(0);
39+
const reader = stream.getReader();
40+
let isDone = false;
41+
while (!isDone) {
42+
const { done, value } = await reader.read();
43+
if (value) {
44+
const prior = res;
45+
res = new Uint8Array(prior.length + value.length);
46+
res.set(prior);
47+
res.set(value, prior.length);
48+
}
49+
isDone = done;
50+
}
51+
return res;
52+
}

packages/util-stream/src/sdk-stream-mixin.browser.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe(sdkStreamMixin.name, () => {
2525
for (const method of transformMethods) {
2626
try {
2727
await sdkStream[method]();
28-
fail(new Error("expect subsequent tranform to fail"));
28+
fail(new Error("expect subsequent transform to fail"));
2929
} catch (error) {
3030
expect(error.message).toContain("The stream has already been transformed");
3131
}
@@ -64,7 +64,7 @@ describe(sdkStreamMixin.name, () => {
6464
sdkStreamMixin({});
6565
fail("expect unexpected stream to fail");
6666
} catch (e) {
67-
expect(e.message).toContain("nexpected stream implementation");
67+
expect(e.message).toContain("unexpected stream implementation");
6868
global.Blob = originalBlobCtr;
6969
}
7070
});
@@ -77,7 +77,7 @@ describe(sdkStreamMixin.name, () => {
7777
expect(byteArray).toEqual(mockStreamCollectorReturn);
7878
});
7979

80-
it("should fail any subsequent tranform calls", async () => {
80+
it("should fail any subsequent transform calls", async () => {
8181
const sdkStream = sdkStreamMixin(payloadStream);
8282
await sdkStream.transformToByteArray();
8383
await expectAllTransformsToFail(sdkStream);
@@ -137,7 +137,7 @@ describe(sdkStreamMixin.name, () => {
137137
}
138138
});
139139

140-
it("should fail any subsequent tranform calls", async () => {
140+
it("should fail any subsequent transform calls", async () => {
141141
const sdkStream = sdkStreamMixin(payloadStream);
142142
await sdkStream.transformToString();
143143
await expectAllTransformsToFail(sdkStream);
@@ -152,7 +152,7 @@ describe(sdkStreamMixin.name, () => {
152152
expect(transformed).toBe(payloadStream);
153153
});
154154

155-
it("should fail any subsequent tranform calls", async () => {
155+
it("should fail any subsequent transform calls", async () => {
156156
const payloadStream = new ReadableStream();
157157
const sdkStream = sdkStreamMixin(payloadStream as any);
158158
sdkStream.transformToWebStream();
@@ -212,7 +212,7 @@ describe(sdkStreamMixin.name, () => {
212212
}
213213
});
214214

215-
it("should fail any subsequent tranform calls", async () => {
215+
it("should fail any subsequent transform calls", async () => {
216216
const payloadStream = new Blob();
217217
const sdkStream = sdkStreamMixin(payloadStream as any);
218218
sdkStream.transformToWebStream();

packages/util-stream/src/sdk-stream-mixin.spec.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe(sdkStreamMixin.name, () => {
2828
for (const method of transformMethods) {
2929
try {
3030
await sdkStream[method]();
31-
fail(new Error("expect subsequent tranform to fail"));
31+
fail(new Error("expect subsequent transform to fail"));
3232
} catch (error) {
3333
expect(error.message).toContain("The stream has already been transformed");
3434
}
@@ -39,6 +39,21 @@ describe(sdkStreamMixin.name, () => {
3939
passThrough = new PassThrough();
4040
});
4141

42+
it("should attempt to use the ReadableStream version if the input is not a Readable", async () => {
43+
if (typeof ReadableStream !== "undefined") {
44+
// ReadableStream is global only as of Node.js 18.
45+
const sdkStream = sdkStreamMixin(
46+
new ReadableStream({
47+
start(controller) {
48+
controller.enqueue(Buffer.from("abcd"));
49+
controller.close();
50+
},
51+
})
52+
);
53+
expect(await sdkStream.transformToByteArray()).toEqual(new Uint8Array([97, 98, 99, 100]));
54+
}
55+
});
56+
4257
it("should throw if unexpected stream implementation is supplied", () => {
4358
try {
4459
const payload = {};
@@ -58,7 +73,7 @@ describe(sdkStreamMixin.name, () => {
5873
expect(await sdkStream.transformToByteArray()).toEqual(expected);
5974
});
6075

61-
it("should fail any subsequent tranform calls", async () => {
76+
it("should fail any subsequent transform calls", async () => {
6277
const sdkStream = sdkStreamMixin(passThrough);
6378
await writeDataToStream(passThrough, [Buffer.from("abc")]);
6479
expect(await sdkStream.transformToByteArray()).toEqual(byteArrayFromBuffer(Buffer.from("abc")));
@@ -108,7 +123,7 @@ describe(sdkStreamMixin.name, () => {
108123
}
109124
);
110125

111-
it("should fail any subsequent tranform calls", async () => {
126+
it("should fail any subsequent transform calls", async () => {
112127
const sdkStream = sdkStreamMixin(passThrough);
113128
await writeDataToStream(passThrough, [Buffer.from("foo")]);
114129
await sdkStream.transformToString();
@@ -164,14 +179,14 @@ describe(sdkStreamMixin.name, () => {
164179
Readable.toWeb = originalToWebImpl;
165180
});
166181

167-
it("should tranform Node stream to web stream", async () => {
182+
it("should transform Node stream to web stream", async () => {
168183
const sdkStream = sdkStreamMixin(passThrough);
169184
sdkStream.transformToWebStream();
170185
// @ts-expect-error
171186
expect(Readable.toWeb).toBeCalled();
172187
});
173188

174-
it("should fail any subsequent tranform calls", async () => {
189+
it("should fail any subsequent transform calls", async () => {
175190
const sdkStream = sdkStreamMixin(passThrough);
176191
await writeDataToStream(passThrough, [Buffer.from("foo")]);
177192
await sdkStream.transformToWebStream();

packages/util-stream/src/sdk-stream-mixin.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,27 @@ import { fromArrayBuffer } from "@smithy/util-buffer-from";
44
import { Readable } from "stream";
55
import { TextDecoder } from "util";
66

7+
import { sdkStreamMixin as sdkStreamMixinReadableStream } from "./sdk-stream-mixin.browser";
8+
79
const ERR_MSG_STREAM_HAS_BEEN_TRANSFORMED = "The stream has already been transformed.";
810

911
/**
1012
* The function that mixes in the utility functions to help consuming runtime-specific payload stream.
1113
*
1214
* @internal
1315
*/
14-
export const sdkStreamMixin = (stream: unknown): SdkStream<Readable> => {
16+
export const sdkStreamMixin = (stream: unknown): SdkStream<ReadableStream | Blob> | SdkStream<Readable> => {
1517
if (!(stream instanceof Readable)) {
16-
// @ts-ignore
17-
const name = stream?.__proto__?.constructor?.name || stream;
18-
throw new Error(`Unexpected stream implementation, expect Stream.Readable instance, got ${name}`);
18+
try {
19+
/**
20+
* If the stream is not node:stream::Readable, it may be a web stream within Node.js.
21+
*/
22+
return sdkStreamMixinReadableStream(stream);
23+
} catch (e: unknown) {
24+
// @ts-ignore
25+
const name = stream?.__proto__?.constructor?.name || stream;
26+
throw new Error(`Unexpected stream implementation, expect Stream.Readable instance, got ${name}`);
27+
}
1928
}
2029

2130
let transformed = false;

0 commit comments

Comments
 (0)