Skip to content

Commit 438598b

Browse files
authored
Allow arbitrary boundaries (#8)
1 parent d962169 commit 438598b

File tree

6 files changed

+212
-167
lines changed

6 files changed

+212
-167
lines changed

src/PatchResolver.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { parseMultipartHttp } from './parseMultipartHttp';
22

3-
export function PatchResolver({ onResponse }) {
3+
export function PatchResolver({ onResponse, boundary }) {
4+
this.boundary = boundary || '-';
45
this.onResponse = onResponse;
56
this.processedChunks = 0;
67
this.chunkBuffer = '';
78
}
89

9-
PatchResolver.prototype.handleChunk = function(data) {
10+
PatchResolver.prototype.handleChunk = function (data) {
1011
this.chunkBuffer += data;
11-
const { newBuffer, parts } = parseMultipartHttp(this.chunkBuffer);
12+
const { newBuffer, parts } = parseMultipartHttp(this.chunkBuffer, this.boundary);
1213
this.chunkBuffer = newBuffer;
1314
if (parts.length) {
1415
this.onResponse(parts);

src/__test__/PatchResolver.spec.js

Lines changed: 157 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { TextEncoder, TextDecoder } from 'util';
44
global.TextEncoder = TextEncoder;
55
global.TextDecoder = TextDecoder;
66

7-
function getMultiPartResponse(data) {
7+
function getMultiPartResponse(data, boundary) {
88
const json = JSON.stringify(data);
99
const chunk = Buffer.from(json, 'utf8');
1010

1111
return [
1212
'',
13-
'---',
13+
`--${boundary}`,
1414
'Content-Type: application/json',
1515
`Content-Length: ${String(chunk.length)}`,
1616
'',
@@ -19,140 +19,160 @@ function getMultiPartResponse(data) {
1919
].join('\r\n');
2020
}
2121

22-
const chunk1Data = {
23-
data: {
24-
viewer: {
25-
currencies: null,
26-
user: {
27-
profile: null,
28-
items: { edges: [{ node: { isFavorite: null } }, { node: { isFavorite: null } }] },
29-
},
30-
},
31-
},
32-
};
33-
const chunk1 = getMultiPartResponse(chunk1Data);
34-
35-
const chunk2Data = {
36-
path: ['viewer', 'currencies'],
37-
data: ['USD', 'GBP', 'EUR', 'CAD', 'AUD', 'CHF', '😂'], // test unicode
38-
errors: [{ message: 'Not So Bad Error' }],
39-
};
40-
const chunk2 = getMultiPartResponse(chunk2Data);
41-
42-
const chunk3Data = { path: ['viewer', 'user', 'profile'], data: { displayName: 'Steven Seagal' } };
43-
const chunk3 = getMultiPartResponse(chunk3Data);
44-
45-
const chunk4Data = {
46-
data: false,
47-
path: ['viewer', 'user', 'items', 'edges', 1, 'node', 'isFavorite'],
48-
};
49-
const chunk4 = getMultiPartResponse(chunk4Data);
50-
51-
describe('PathResolver', function() {
52-
it('should work on each chunk', function() {
53-
const onResponse = jest.fn();
54-
const resolver = new PatchResolver({
55-
onResponse,
22+
describe('PathResolver', function () {
23+
for (const boundary of ['-', 'gc0p4Jq0M2Yt08jU534c0p']) {
24+
describe(`boundary ${boundary}`, () => {
25+
const chunk1Data = {
26+
data: {
27+
viewer: {
28+
currencies: null,
29+
user: {
30+
profile: null,
31+
items: {
32+
edges: [
33+
{ node: { isFavorite: null } },
34+
{ node: { isFavorite: null } },
35+
],
36+
},
37+
},
38+
},
39+
},
40+
};
41+
const chunk1 = getMultiPartResponse(chunk1Data, boundary);
42+
43+
const chunk2Data = {
44+
path: ['viewer', 'currencies'],
45+
data: ['USD', 'GBP', 'EUR', 'CAD', 'AUD', 'CHF', '😂'], // test unicode
46+
errors: [{ message: 'Not So Bad Error' }],
47+
};
48+
const chunk2 = getMultiPartResponse(chunk2Data, boundary);
49+
50+
const chunk3Data = {
51+
path: ['viewer', 'user', 'profile'],
52+
data: { displayName: 'Steven Seagal' },
53+
};
54+
const chunk3 = getMultiPartResponse(chunk3Data, boundary);
55+
56+
const chunk4Data = {
57+
data: false,
58+
path: ['viewer', 'user', 'items', 'edges', 1, 'node', 'isFavorite'],
59+
};
60+
const chunk4 = getMultiPartResponse(chunk4Data, boundary);
61+
it('should work on each chunk', function () {
62+
const onResponse = jest.fn();
63+
const resolver = new PatchResolver({
64+
onResponse,
65+
boundary,
66+
});
67+
68+
resolver.handleChunk(chunk1);
69+
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
70+
71+
onResponse.mockClear();
72+
resolver.handleChunk(chunk2);
73+
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
74+
75+
onResponse.mockClear();
76+
resolver.handleChunk(chunk3);
77+
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
78+
79+
onResponse.mockClear();
80+
resolver.handleChunk(chunk4);
81+
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
82+
});
83+
84+
it('should work when chunks are split', function () {
85+
const onResponse = jest.fn();
86+
const resolver = new PatchResolver({
87+
onResponse,
88+
boundary,
89+
});
90+
91+
if (boundary === 'gc0p4Jq0M2Yt08jU534c0p') {
92+
debugger;
93+
}
94+
95+
const chunk1a = chunk1.substr(0, 35);
96+
const chunk1b = chunk1.substr(35, 80);
97+
const chunk1c = chunk1.substr(35 + 80);
98+
99+
resolver.handleChunk(chunk1a);
100+
expect(onResponse).not.toHaveBeenCalled();
101+
resolver.handleChunk(chunk1b);
102+
expect(onResponse).not.toHaveBeenCalled();
103+
resolver.handleChunk(chunk1c);
104+
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
105+
onResponse.mockClear();
106+
107+
const chunk2a = chunk2.substr(0, 35);
108+
const chunk2b = chunk2.substr(35);
109+
110+
resolver.handleChunk(chunk2a);
111+
expect(onResponse).not.toHaveBeenCalled();
112+
resolver.handleChunk(chunk2b);
113+
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
114+
onResponse.mockClear();
115+
116+
const chunk3a = chunk3.substr(0, 10);
117+
const chunk3b = chunk3.substr(10, 20);
118+
const chunk3c = chunk3.substr(10 + 20);
119+
120+
resolver.handleChunk(chunk3a);
121+
expect(onResponse).not.toHaveBeenCalled();
122+
resolver.handleChunk(chunk3b);
123+
expect(onResponse).not.toHaveBeenCalled();
124+
resolver.handleChunk(chunk3c);
125+
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
126+
});
127+
128+
it('should work when chunks are combined', function () {
129+
const onResponse = jest.fn();
130+
const resolver = new PatchResolver({
131+
onResponse,
132+
boundary,
133+
});
134+
135+
resolver.handleChunk(chunk1 + chunk2);
136+
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
137+
});
138+
139+
it('should work when chunks are combined and split', function () {
140+
const onResponse = jest.fn();
141+
const resolver = new PatchResolver({
142+
onResponse,
143+
boundary,
144+
});
145+
146+
const chunk3a = chunk3.substr(0, 11);
147+
const chunk3b = chunk3.substr(11, 20);
148+
const chunk3c = chunk3.substr(11 + 20);
149+
150+
resolver.handleChunk(chunk1 + chunk2 + chunk3a);
151+
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
152+
onResponse.mockClear();
153+
154+
resolver.handleChunk(chunk3b);
155+
expect(onResponse).not.toHaveBeenCalled();
156+
resolver.handleChunk(chunk3c);
157+
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
158+
});
159+
160+
it('should work when chunks are combined across boundaries', function () {
161+
const onResponse = jest.fn();
162+
const resolver = new PatchResolver({
163+
onResponse,
164+
boundary,
165+
});
166+
167+
const chunk2a = chunk2.substring(0, 35);
168+
const chunk2b = chunk2.substring(35);
169+
170+
resolver.handleChunk(chunk1 + chunk2a);
171+
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
172+
onResponse.mockClear();
173+
resolver.handleChunk(chunk2b);
174+
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
175+
});
56176
});
57-
58-
resolver.handleChunk(chunk1);
59-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
60-
61-
onResponse.mockClear();
62-
resolver.handleChunk(chunk2);
63-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
64-
65-
onResponse.mockClear();
66-
resolver.handleChunk(chunk3);
67-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
68-
69-
onResponse.mockClear();
70-
resolver.handleChunk(chunk4);
71-
expect(onResponse.mock.calls[0][0]).toEqual([chunk4Data]);
72-
});
73-
74-
it('should work when chunks are split', function() {
75-
const onResponse = jest.fn();
76-
const resolver = new PatchResolver({
77-
onResponse,
78-
});
79-
80-
const chunk1a = chunk1.substr(0, 35);
81-
const chunk1b = chunk1.substr(35, 80);
82-
const chunk1c = chunk1.substr(35 + 80);
83-
84-
resolver.handleChunk(chunk1a);
85-
expect(onResponse).not.toHaveBeenCalled();
86-
resolver.handleChunk(chunk1b);
87-
expect(onResponse).not.toHaveBeenCalled();
88-
resolver.handleChunk(chunk1c);
89-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
90-
onResponse.mockClear();
91-
92-
const chunk2a = chunk2.substr(0, 35);
93-
const chunk2b = chunk2.substr(35);
94-
95-
resolver.handleChunk(chunk2a);
96-
expect(onResponse).not.toHaveBeenCalled();
97-
resolver.handleChunk(chunk2b);
98-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
99-
onResponse.mockClear();
100-
101-
const chunk3a = chunk3.substr(0, 10);
102-
const chunk3b = chunk3.substr(11, 20);
103-
const chunk3c = chunk3.substr(11 + 20);
104-
105-
resolver.handleChunk(chunk3a);
106-
expect(onResponse).not.toHaveBeenCalled();
107-
resolver.handleChunk(chunk3b);
108-
expect(onResponse).not.toHaveBeenCalled();
109-
resolver.handleChunk(chunk3c);
110-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
111-
});
112-
113-
it('should work when chunks are combined', function() {
114-
const onResponse = jest.fn();
115-
const resolver = new PatchResolver({
116-
onResponse,
117-
});
118-
119-
resolver.handleChunk(chunk1 + chunk2);
120-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
121-
});
122-
123-
it('should work when chunks are combined and split', function() {
124-
const onResponse = jest.fn();
125-
const resolver = new PatchResolver({
126-
onResponse,
127-
});
128-
129-
const chunk3a = chunk3.substr(0, 10);
130-
const chunk3b = chunk3.substr(11, 20);
131-
const chunk3c = chunk3.substr(11 + 20);
132-
133-
resolver.handleChunk(chunk1 + chunk2 + chunk3a);
134-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data, chunk2Data]);
135-
onResponse.mockClear();
136-
137-
resolver.handleChunk(chunk3b);
138-
expect(onResponse).not.toHaveBeenCalled();
139-
resolver.handleChunk(chunk3c);
140-
expect(onResponse.mock.calls[0][0]).toEqual([chunk3Data]);
141-
});
142-
143-
it('should work when chunks are combined across boundaries', function() {
144-
const onResponse = jest.fn();
145-
const resolver = new PatchResolver({
146-
onResponse,
147-
});
148-
149-
const chunk2a = chunk2.substring(0, 35);
150-
const chunk2b = chunk2.substring(35);
151-
152-
resolver.handleChunk(chunk1 + chunk2a);
153-
expect(onResponse.mock.calls[0][0]).toEqual([chunk1Data]);
154-
onResponse.mockClear();
155-
resolver.handleChunk(chunk2b);
156-
expect(onResponse.mock.calls[0][0]).toEqual([chunk2Data]);
157-
});
177+
}
158178
});

src/fetch.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import { PatchResolver } from './PatchResolver';
2+
import { getBoundary } from './getBoundary';
23

34
export function fetchImpl(
45
url,
5-
{ method, headers, credentials, body, onNext, onError, onComplete, applyToPrevious }
6+
{ method, headers, credentials, body, onNext, onError, onComplete }
67
) {
78
return fetch(url, { method, headers, body, credentials })
8-
.then(response => {
9+
.then((response) => {
10+
const contentType = (!!response.headers && response.headers.get('Content-Type')) || '';
911
// @defer uses multipart responses to stream patches over HTTP
10-
if (
11-
response.status < 300 &&
12-
response.headers &&
13-
response.headers.get('Content-Type') &&
14-
response.headers.get('Content-Type').indexOf('multipart/mixed') >= 0
15-
) {
12+
if (response.status < 300 && contentType.indexOf('multipart/mixed') >= 0) {
13+
const boundary = getBoundary(contentType);
14+
1615
// For the majority of browsers with support for ReadableStream and TextDecoder
1716
const reader = response.body.getReader();
1817
const textDecoder = new TextDecoder();
1918
const patchResolver = new PatchResolver({
20-
onResponse: r => onNext(r),
21-
applyToPrevious,
19+
onResponse: (r) => onNext(r),
20+
boundary,
2221
});
2322
return reader.read().then(function sendNext({ value, done }) {
2423
if (!done) {
@@ -40,7 +39,7 @@ export function fetchImpl(
4039
}
4140
});
4241
} else {
43-
return response.json().then(json => {
42+
return response.json().then((json) => {
4443
onNext([json]);
4544
onComplete();
4645
});

src/getBoundary.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { xhrImpl } from './xhr';
2+
import { fetchImpl } from './fetch';
3+
4+
export function getBoundary(contentType = '') {
5+
const contentTypeParts = contentType.split(';');
6+
for (const contentTypePart of contentTypeParts) {
7+
const [key, value] = (contentTypePart || '').trim().split('=');
8+
if (key === 'boundary' && !!value) {
9+
if (value[0] === '"' && value[value.length - 1] === '"') {
10+
return value.substr(1, value.length - 2);
11+
}
12+
return value;
13+
}
14+
}
15+
return '-';
16+
}

0 commit comments

Comments
 (0)