Skip to content

Commit

Permalink
feat: remove the need for content-length (#22)
Browse files Browse the repository at this point in the history
* feat: remove the need for content-length

* chore: updates tests to more discrete
  • Loading branch information
maraisr authored Dec 10, 2020
1 parent e7435f7 commit 6de3715
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 63 deletions.
5 changes: 3 additions & 2 deletions src/PatchResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { parseMultipartHttp } from './parseMultipartHttp';
export function PatchResolver({ onResponse, boundary }) {
this.boundary = boundary || '-';
this.onResponse = onResponse;
this.processedChunks = 0;
this.chunkBuffer = '';
this.isPreamble = true;
}

PatchResolver.prototype.handleChunk = function (data) {
this.chunkBuffer += data;
const { newBuffer, parts } = parseMultipartHttp(this.chunkBuffer, this.boundary);
const { newBuffer, parts, isPreamble } = parseMultipartHttp(this.chunkBuffer, this.boundary, [], this.isPreamble);
this.isPreamble = isPreamble;
this.chunkBuffer = newBuffer;
if (parts.length) {
this.onResponse(parts);
Expand Down
38 changes: 29 additions & 9 deletions src/__test__/PatchResolver.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@ global.TextDecoder = TextDecoder;

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

return [
'',
`--${boundary}`,
'Content-Type: application/json',
`Content-Length: ${String(chunk.length)}`,
'',
json,
'',
`--${boundary}\r\n`,
].join('\r\n');
}

Expand Down Expand Up @@ -65,6 +61,10 @@ describe('PathResolver', function () {
boundary,
});

resolver.handleChunk(`\r\n--${boundary}\r\n`);

expect(onResponse).not.toHaveBeenCalled();

resolver.handleChunk(chunk1);
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);

Expand All @@ -88,14 +88,14 @@ describe('PathResolver', function () {
boundary,
});

if (boundary === 'gc0p4Jq0M2Yt08jU534c0p') {
debugger;
}

const chunk1a = chunk1.substr(0, 35);
const chunk1b = chunk1.substr(35, 80);
const chunk1c = chunk1.substr(35 + 80);

resolver.handleChunk(`\r\n--${boundary}\r\n`);

expect(onResponse).not.toHaveBeenCalled();

resolver.handleChunk(chunk1a);
expect(onResponse).not.toHaveBeenCalled();
resolver.handleChunk(chunk1b);
Expand Down Expand Up @@ -132,6 +132,10 @@ describe('PathResolver', function () {
boundary,
});

resolver.handleChunk(`\r\n--${boundary}\r\n`);

expect(onResponse).not.toHaveBeenCalled();

resolver.handleChunk(chunk1 + chunk2);
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
});
Expand All @@ -143,10 +147,22 @@ describe('PathResolver', function () {
boundary,
});

const boundaryChunk = `\r\n--${boundary}\r\n`;

const chunk3a = chunk3.substr(0, 11);
const chunk3b = chunk3.substr(11, 20);
const chunk3c = chunk3.substr(11 + 20);

const boundary1 = boundaryChunk.substr(0, 5);
const boundary2 = boundaryChunk.substr(5, 7);
const boundary3 = boundaryChunk.substr(5 + 7);

resolver.handleChunk(boundary1);
resolver.handleChunk(boundary2);
resolver.handleChunk(boundary3);

expect(onResponse).not.toHaveBeenCalled();

resolver.handleChunk(chunk1 + chunk2 + chunk3a);
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
onResponse.mockClear();
Expand All @@ -164,6 +180,10 @@ describe('PathResolver', function () {
boundary,
});

resolver.handleChunk(`\r\n--${boundary}\r\n`);

expect(onResponse).not.toHaveBeenCalled();

const chunk2a = chunk2.substring(0, 35);
const chunk2b = chunk2.substring(35);

Expand Down
80 changes: 28 additions & 52 deletions src/parseMultipartHttp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,51 @@ function getDelimiter(boundary) {
return `\r\n--${boundary}\r\n`;
}

function getFinalDelimiter(boundary) {
return `\r\n--${boundary}--\r\n`;
}

function splitWithRest(string, delim) {
const index = string.indexOf(delim);
if (index < 0) {
return [string];
return [undefined, string];
}
return [string.substring(0, index), string.substring(index + delim.length)];
}

export function parseMultipartHttp(buffer, boundary, previousParts = []) {
const delimeter = getDelimiter(boundary);
let [, rest] = splitWithRest(buffer, delimeter);
if (!(rest && rest.length)) {
// we did not finish receiving the initial delimeter
return {
newBuffer: buffer,
parts: previousParts,
};
}
const parts = splitWithRest(rest, '\r\n\r\n');
const headers = parts[0];
rest = parts[1];
export function parseMultipartHttp(buffer, boundary, previousParts = [], isPreamble = true) {
const delimiter = getDelimiter(boundary);

if (!(rest && rest.length)) {
// we did not finish receiving the headers
return {
newBuffer: buffer,
parts: previousParts,
};
}
const [region, next] = splitWithRest(buffer, delimiter);

const headersArr = headers.split('\r\n');
const contentLengthHeader = headersArr.find(
(headerLine) => headerLine.toLowerCase().indexOf('content-length:') >= 0
);
if (contentLengthHeader === undefined) {
throw new Error('Invalid MultiPart Response, no content-length header');
}
const contentLengthArr = contentLengthHeader.split(':');
let contentLength;
if (contentLengthArr.length === 2 && !isNaN(parseInt(contentLengthArr[1]))) {
contentLength = parseInt(contentLengthArr[1]);
} else {
throw new Error('Invalid MultiPart Response, could not parse content-length');
if (region !== undefined && (region.length || region.trim() === '') && isPreamble) {
if (next && next.length) {
// if we have stuff after the boundary; and we're in preamble—we recurse
return parseMultipartHttp(next, boundary, previousParts, false);
} else {
return { newBuffer: '', parts: previousParts, isPreamble: false };
}
}

// Strip out the final delimiter
const finalDelimeter = getFinalDelimiter(boundary);
rest = rest.replace(finalDelimeter, '');
const uint = new TextEncoder().encode(rest);

if (uint.length < contentLength) {
// still waiting for more body to be sent;
if (!(region && region.length)) {
// we need more things
return {
newBuffer: buffer,
parts: previousParts,
isPreamble
};
}

const body = new TextDecoder().decode(uint.subarray(0, contentLength));
const nextBuffer = new TextDecoder().decode(uint.subarray(contentLength));
const part = JSON.parse(body);
const newParts = [...previousParts, part];
let [_headers, body] = splitWithRest(region, '\r\n\r\n');

// remove trailing boundary things
body = body
.replace(delimiter + '\r\n', '')
.replace(delimiter + '--\r\n', '');

if (nextBuffer.length) {
return parseMultipartHttp(nextBuffer, boundary, newParts);
const payload = JSON.parse(body);
const parts = [...previousParts, payload];

if (next && next.length) {
// we have more parts
return parseMultipartHttp(next, boundary, parts, isPreamble);
}
return { parts: newParts, newBuffer: '' };

return { parts, newBuffer: '', isPreamble };
}

0 comments on commit 6de3715

Please sign in to comment.