Skip to content

Commit 6de3715

Browse files
authored
feat: remove the need for content-length (#22)
* feat: remove the need for content-length * chore: updates tests to more discrete
1 parent e7435f7 commit 6de3715

File tree

3 files changed

+60
-63
lines changed

3 files changed

+60
-63
lines changed

src/PatchResolver.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { parseMultipartHttp } from './parseMultipartHttp';
33
export function PatchResolver({ onResponse, boundary }) {
44
this.boundary = boundary || '-';
55
this.onResponse = onResponse;
6-
this.processedChunks = 0;
76
this.chunkBuffer = '';
7+
this.isPreamble = true;
88
}
99

1010
PatchResolver.prototype.handleChunk = function (data) {
1111
this.chunkBuffer += data;
12-
const { newBuffer, parts } = parseMultipartHttp(this.chunkBuffer, this.boundary);
12+
const { newBuffer, parts, isPreamble } = parseMultipartHttp(this.chunkBuffer, this.boundary, [], this.isPreamble);
13+
this.isPreamble = isPreamble;
1314
this.chunkBuffer = newBuffer;
1415
if (parts.length) {
1516
this.onResponse(parts);

src/__test__/PatchResolver.spec.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@ global.TextDecoder = TextDecoder;
66

77
function getMultiPartResponse(data, boundary) {
88
const json = JSON.stringify(data);
9-
const chunk = Buffer.from(json, 'utf8');
109

1110
return [
12-
'',
13-
`--${boundary}`,
1411
'Content-Type: application/json',
15-
`Content-Length: ${String(chunk.length)}`,
1612
'',
1713
json,
18-
'',
14+
`--${boundary}\r\n`,
1915
].join('\r\n');
2016
}
2117

@@ -65,6 +61,10 @@ describe('PathResolver', function () {
6561
boundary,
6662
});
6763

64+
resolver.handleChunk(`\r\n--${boundary}\r\n`);
65+
66+
expect(onResponse).not.toHaveBeenCalled();
67+
6868
resolver.handleChunk(chunk1);
6969
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
7070

@@ -88,14 +88,14 @@ describe('PathResolver', function () {
8888
boundary,
8989
});
9090

91-
if (boundary === 'gc0p4Jq0M2Yt08jU534c0p') {
92-
debugger;
93-
}
94-
9591
const chunk1a = chunk1.substr(0, 35);
9692
const chunk1b = chunk1.substr(35, 80);
9793
const chunk1c = chunk1.substr(35 + 80);
9894

95+
resolver.handleChunk(`\r\n--${boundary}\r\n`);
96+
97+
expect(onResponse).not.toHaveBeenCalled();
98+
9999
resolver.handleChunk(chunk1a);
100100
expect(onResponse).not.toHaveBeenCalled();
101101
resolver.handleChunk(chunk1b);
@@ -132,6 +132,10 @@ describe('PathResolver', function () {
132132
boundary,
133133
});
134134

135+
resolver.handleChunk(`\r\n--${boundary}\r\n`);
136+
137+
expect(onResponse).not.toHaveBeenCalled();
138+
135139
resolver.handleChunk(chunk1 + chunk2);
136140
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
137141
});
@@ -143,10 +147,22 @@ describe('PathResolver', function () {
143147
boundary,
144148
});
145149

150+
const boundaryChunk = `\r\n--${boundary}\r\n`;
151+
146152
const chunk3a = chunk3.substr(0, 11);
147153
const chunk3b = chunk3.substr(11, 20);
148154
const chunk3c = chunk3.substr(11 + 20);
149155

156+
const boundary1 = boundaryChunk.substr(0, 5);
157+
const boundary2 = boundaryChunk.substr(5, 7);
158+
const boundary3 = boundaryChunk.substr(5 + 7);
159+
160+
resolver.handleChunk(boundary1);
161+
resolver.handleChunk(boundary2);
162+
resolver.handleChunk(boundary3);
163+
164+
expect(onResponse).not.toHaveBeenCalled();
165+
150166
resolver.handleChunk(chunk1 + chunk2 + chunk3a);
151167
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
152168
onResponse.mockClear();
@@ -164,6 +180,10 @@ describe('PathResolver', function () {
164180
boundary,
165181
});
166182

183+
resolver.handleChunk(`\r\n--${boundary}\r\n`);
184+
185+
expect(onResponse).not.toHaveBeenCalled();
186+
167187
const chunk2a = chunk2.substring(0, 35);
168188
const chunk2b = chunk2.substring(35);
169189

src/parseMultipartHttp.js

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,75 +2,51 @@ function getDelimiter(boundary) {
22
return `\r\n--${boundary}\r\n`;
33
}
44

5-
function getFinalDelimiter(boundary) {
6-
return `\r\n--${boundary}--\r\n`;
7-
}
8-
95
function splitWithRest(string, delim) {
106
const index = string.indexOf(delim);
117
if (index < 0) {
12-
return [string];
8+
return [undefined, string];
139
}
1410
return [string.substring(0, index), string.substring(index + delim.length)];
1511
}
1612

17-
export function parseMultipartHttp(buffer, boundary, previousParts = []) {
18-
const delimeter = getDelimiter(boundary);
19-
let [, rest] = splitWithRest(buffer, delimeter);
20-
if (!(rest && rest.length)) {
21-
// we did not finish receiving the initial delimeter
22-
return {
23-
newBuffer: buffer,
24-
parts: previousParts,
25-
};
26-
}
27-
const parts = splitWithRest(rest, '\r\n\r\n');
28-
const headers = parts[0];
29-
rest = parts[1];
13+
export function parseMultipartHttp(buffer, boundary, previousParts = [], isPreamble = true) {
14+
const delimiter = getDelimiter(boundary);
3015

31-
if (!(rest && rest.length)) {
32-
// we did not finish receiving the headers
33-
return {
34-
newBuffer: buffer,
35-
parts: previousParts,
36-
};
37-
}
16+
const [region, next] = splitWithRest(buffer, delimiter);
3817

39-
const headersArr = headers.split('\r\n');
40-
const contentLengthHeader = headersArr.find(
41-
(headerLine) => headerLine.toLowerCase().indexOf('content-length:') >= 0
42-
);
43-
if (contentLengthHeader === undefined) {
44-
throw new Error('Invalid MultiPart Response, no content-length header');
45-
}
46-
const contentLengthArr = contentLengthHeader.split(':');
47-
let contentLength;
48-
if (contentLengthArr.length === 2 && !isNaN(parseInt(contentLengthArr[1]))) {
49-
contentLength = parseInt(contentLengthArr[1]);
50-
} else {
51-
throw new Error('Invalid MultiPart Response, could not parse content-length');
18+
if (region !== undefined && (region.length || region.trim() === '') && isPreamble) {
19+
if (next && next.length) {
20+
// if we have stuff after the boundary; and we're in preamble—we recurse
21+
return parseMultipartHttp(next, boundary, previousParts, false);
22+
} else {
23+
return { newBuffer: '', parts: previousParts, isPreamble: false };
24+
}
5225
}
5326

54-
// Strip out the final delimiter
55-
const finalDelimeter = getFinalDelimiter(boundary);
56-
rest = rest.replace(finalDelimeter, '');
57-
const uint = new TextEncoder().encode(rest);
58-
59-
if (uint.length < contentLength) {
60-
// still waiting for more body to be sent;
27+
if (!(region && region.length)) {
28+
// we need more things
6129
return {
6230
newBuffer: buffer,
6331
parts: previousParts,
32+
isPreamble
6433
};
6534
}
6635

67-
const body = new TextDecoder().decode(uint.subarray(0, contentLength));
68-
const nextBuffer = new TextDecoder().decode(uint.subarray(contentLength));
69-
const part = JSON.parse(body);
70-
const newParts = [...previousParts, part];
36+
let [_headers, body] = splitWithRest(region, '\r\n\r\n');
37+
38+
// remove trailing boundary things
39+
body = body
40+
.replace(delimiter + '\r\n', '')
41+
.replace(delimiter + '--\r\n', '');
7142

72-
if (nextBuffer.length) {
73-
return parseMultipartHttp(nextBuffer, boundary, newParts);
43+
const payload = JSON.parse(body);
44+
const parts = [...previousParts, payload];
45+
46+
if (next && next.length) {
47+
// we have more parts
48+
return parseMultipartHttp(next, boundary, parts, isPreamble);
7449
}
75-
return { parts: newParts, newBuffer: '' };
50+
51+
return { parts, newBuffer: '', isPreamble };
7652
}

0 commit comments

Comments
 (0)