Skip to content

Commit 3367440

Browse files
e3diodarrachequesne
authored andcommitted
fix(uws): properly handle chunked content (#642)
With the engine based on µWebSockets.js (introduced in version 6.1.0), a huge request body split in multiple chunks would throw the following error: > node:buffer:254 > TypedArrayPrototypeSet(target, source, targetStart); > ^ > > TypeError: Cannot perform %TypedArray%.prototype.set on a detached ArrayBuffer > at Buffer.set (<anonymous>) > at _copyActual (node:buffer:254:3) > node:buffer:254 > TypedArrayPrototypeSet(target, source, targetStart); > ^ > > TypeError: Cannot perform %TypedArray%.prototype.set on a detached ArrayBuffer > at Buffer.set (<anonymous>) > at _copyActual (node:buffer:254:3) > at Function.concat (node:buffer:562:12) > at onEnd (.../node_modules/engine.io/build/transports-uws/polling.js:126:32) > at .../node_modules/engine.io/build/transports-uws/polling.js:143:17 Note: µWebSockets.js does not currently support chunked transfer encoding.
1 parent a463d26 commit 3367440

File tree

2 files changed

+104
-13
lines changed

2 files changed

+104
-13
lines changed

lib/transports-uws/polling.ts

+45-13
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,20 @@ export class Polling extends Transport {
122122
return;
123123
}
124124

125+
const expectedContentLength = Number(req.headers["content-length"]);
126+
127+
if (!expectedContentLength) {
128+
this.onError("content-length header required");
129+
res.writeStatus("411 Length Required").end();
130+
return;
131+
}
132+
133+
if (expectedContentLength > this.maxHttpBufferSize) {
134+
this.onError("payload too large");
135+
res.writeStatus("413 Payload Too Large").end();
136+
return;
137+
}
138+
125139
const isBinary = "application/octet-stream" === req.headers["content-type"];
126140

127141
if (isBinary && this.protocol === 4) {
@@ -131,11 +145,11 @@ export class Polling extends Transport {
131145
this.dataReq = req;
132146
this.dataRes = res;
133147

134-
let chunks = [];
135-
let contentLength = 0;
148+
let buffer;
149+
let offset = 0;
136150

137151
const cleanup = () => {
138-
this.dataReq = this.dataRes = chunks = null;
152+
this.dataReq = this.dataRes = null;
139153
};
140154

141155
const onClose = () => {
@@ -154,8 +168,8 @@ export class Polling extends Transport {
154168
res.writeHeader(key, String(headers[key]));
155169
});
156170

157-
const onEnd = () => {
158-
this.onData(Buffer.concat(chunks).toString());
171+
const onEnd = buffer => {
172+
this.onData(buffer.toString());
159173

160174
if (this.readyState !== "closing") {
161175
res.end("ok");
@@ -165,18 +179,36 @@ export class Polling extends Transport {
165179

166180
res.onAborted(onClose);
167181

168-
res.onData((chunk, isLast) => {
169-
chunks.push(Buffer.from(chunk));
170-
contentLength += Buffer.byteLength(chunk);
171-
if (contentLength > this.maxHttpBufferSize) {
172-
this.onError("payload too large");
173-
res.writeStatus("413 Payload Too Large");
174-
res.end();
182+
res.onData((arrayBuffer, isLast) => {
183+
const totalLength = offset + arrayBuffer.byteLength;
184+
if (totalLength > expectedContentLength) {
185+
this.onError("content-length mismatch");
186+
res.close(); // calls onAborted
175187
return;
176188
}
189+
190+
if (!buffer) {
191+
if (isLast) {
192+
onEnd(Buffer.from(arrayBuffer));
193+
return;
194+
}
195+
buffer = Buffer.allocUnsafe(expectedContentLength);
196+
}
197+
198+
Buffer.from(arrayBuffer).copy(buffer, offset);
199+
177200
if (isLast) {
178-
onEnd();
201+
if (totalLength != expectedContentLength) {
202+
this.onError("content-length mismatch");
203+
res.writeStatus("400 Content-Length Mismatch").end();
204+
cleanup();
205+
return;
206+
}
207+
onEnd(buffer);
208+
return;
179209
}
210+
211+
offset = totalLength;
180212
});
181213
}
182214

test/server.js

+59
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,65 @@ describe("server", () => {
19551955
});
19561956
});
19571957

1958+
it("should arrive when content is split in multiple chunks (polling)", done => {
1959+
const engine = listen(
1960+
{
1961+
maxHttpBufferSize: 1e10
1962+
},
1963+
port => {
1964+
const client = new ClientSocket(`ws://localhost:${port}`, {
1965+
transports: ["polling"]
1966+
});
1967+
1968+
engine.on("connection", socket => {
1969+
socket.on("message", data => {
1970+
client.close();
1971+
done();
1972+
});
1973+
});
1974+
1975+
client.on("open", () => {
1976+
client.send("a".repeat(1e6));
1977+
});
1978+
}
1979+
);
1980+
});
1981+
1982+
it("should arrive when content is sent with chunked transfer-encoding (polling)", function(done) {
1983+
if (process.env.EIO_WS_ENGINE === "uws") {
1984+
// µWebSockets.js does not currently support chunked encoding: https://github.com/uNetworking/uWebSockets.js/issues/669
1985+
return this.skip();
1986+
}
1987+
const engine = listen(port => {
1988+
const client = new ClientSocket(`ws://localhost:${port}`, {
1989+
transports: ["polling"]
1990+
});
1991+
1992+
engine.on("connection", socket => {
1993+
socket.on("message", data => {
1994+
expect(data).to.eql("123");
1995+
1996+
client.close();
1997+
done();
1998+
});
1999+
});
2000+
2001+
client.on("open", () => {
2002+
const req = http.request({
2003+
host: "localhost",
2004+
port,
2005+
path: `/engine.io/?EIO=4&transport=polling&sid=${client.id}`,
2006+
method: "POST"
2007+
});
2008+
2009+
req.write(process.env.EIO_CLIENT === "3" ? "4:41" : "41");
2010+
req.write("2");
2011+
req.write("3");
2012+
req.end();
2013+
});
2014+
});
2015+
});
2016+
19582017
it("should arrive as ArrayBuffer if requested when binary data sent as Buffer (polling)", done => {
19592018
const binaryData = Buffer.allocUnsafe(5);
19602019
for (let i = 0; i < binaryData.length; i++) {

0 commit comments

Comments
 (0)