Skip to content

Commit d970d66

Browse files
e3diodarrachequesne
authored andcommitted
fix(uws): properly handle chunked content
On Safari, a POST request would result in two chunks (the first being empty), and threw 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) > at Function.concat (node:buffer:562:12) > at onEnd (xxx/node_modules/engine.io/build/transports-uws/polling.js:126:32) > at xxx/node_modules/engine.io/build/transports-uws/polling.js:143:17 Which is a bit weird, because it seems µWebSockets.js does not support chunked content: uNetworking/uWebSockets.js#669
1 parent a463d26 commit d970d66

File tree

2 files changed

+80
-13
lines changed

2 files changed

+80
-13
lines changed

lib/transports-uws/polling.ts

Lines changed: 45 additions & 13 deletions
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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,41 @@ describe("server", () => {
19551955
});
19561956
});
19571957

1958+
it.only("should arrive when content is chunked", function(done) {
1959+
if (process.env.EIO_WS_ENGINE === "uws") {
1960+
// µWebSockets.js does not currently support chunked encoding: https://github.com/uNetworking/uWebSockets.js/issues/669
1961+
return this.skip();
1962+
}
1963+
const engine = listen(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+
expect(data).to.eql("123");
1971+
1972+
client.close();
1973+
done();
1974+
});
1975+
});
1976+
1977+
client.on("open", () => {
1978+
const req = http.request({
1979+
host: "localhost",
1980+
port,
1981+
path: `/engine.io/?EIO=4&transport=polling&sid=${client.id}`,
1982+
method: "POST"
1983+
});
1984+
1985+
req.write(process.env.EIO_CLIENT === "3" ? "4:41" : "41");
1986+
req.write("2");
1987+
req.write("3");
1988+
req.end();
1989+
});
1990+
});
1991+
});
1992+
19581993
it("should arrive as ArrayBuffer if requested when binary data sent as Buffer (polling)", done => {
19591994
const binaryData = Buffer.allocUnsafe(5);
19601995
for (let i = 0; i < binaryData.length; i++) {

0 commit comments

Comments
 (0)