Skip to content

Commit 1b912ac

Browse files
authored
Merge pull request #34 from simplabs/chunked-response
Enable chunked response
2 parents 5a0981d + 1fc8b20 commit 1b912ac

5 files changed

Lines changed: 250 additions & 181 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,22 @@ app.get('/*', middleware);
8989
fastboot.reload();
9090
```
9191

92+
## Response chunking
93+
94+
By default, the middleware writes the complete response at once but response
95+
chunking (aka HTTP Streaming) is available via a config switch:
96+
97+
```js
98+
app.get('/*', fastbootMiddleware({
99+
distPath: '/path/to/dist',
100+
chunkedResponse: true
101+
}));
102+
```
103+
104+
Enabling response chunking will result in the response being delivered in
105+
multiple chunks (one for the head, one for the body and one for each shoebox)
106+
which helps getting the HTML to clients faster.
107+
92108
[ember-cli-fastboot]: https://github.com/ember-fastboot/ember-cli-fastboot
93109

94110
## Tests

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
},
3939
"dependencies": {
4040
"chalk": "^2.0.1",
41-
"fastboot": "^1.1.0",
41+
"fastboot": "^1.1.2",
4242
"request": "^2.81.0"
4343
}
4444
}

src/index.js

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,28 +32,37 @@ function fastbootExpressMiddleware(distPath, options) {
3232
.then(success, failure);
3333

3434
function success(result) {
35-
result.html()
36-
.then(html => {
37-
let headers = result.headers;
38-
let statusMessage = result.error ? 'NOT OK ' : 'OK ';
39-
40-
for (var pair of headers.entries()) {
41-
res.set(pair[0], pair[1]);
42-
}
43-
44-
if (result.error) {
45-
log("RESILIENT MODE CAUGHT:", result.error.stack);
46-
next(result.error);
47-
}
48-
49-
log(result.statusCode, statusMessage + path);
50-
res.status(result.statusCode);
51-
res.send(html);
52-
})
53-
.catch(error => {
54-
res.status(500);
55-
next(error);
56-
});
35+
let responseBody = opts.chunkedResponse ? result.chunks() : result.html();
36+
37+
responseBody.then(body => {
38+
let headers = result.headers;
39+
let statusMessage = result.error ? 'NOT OK ' : 'OK ';
40+
41+
for (var pair of headers.entries()) {
42+
res.set(pair[0], pair[1]);
43+
}
44+
45+
if (result.error) {
46+
log("RESILIENT MODE CAUGHT:", result.error.stack);
47+
next(result.error);
48+
}
49+
50+
log(result.statusCode, statusMessage + path);
51+
res.status(result.statusCode);
52+
53+
if (typeof body === 'string') {
54+
res.send(body);
55+
} else if (result.error) {
56+
res.send(body[0]);
57+
} else {
58+
body.forEach(chunk => res.write(chunk));
59+
res.end();
60+
}
61+
})
62+
.catch(error => {
63+
res.status(500);
64+
next(error);
65+
});
5766
}
5867

5968
function failure(error) {

test/middleware-test.js

Lines changed: 134 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -51,30 +51,6 @@ describe("FastBoot", function() {
5151
});
5252
});
5353

54-
it("returns 404 when navigating to a URL that doesn't exist", function() {
55-
let middleware = fastbootMiddleware(fixture('basic-app'));
56-
server = new TestHTTPServer(middleware);
57-
58-
return server.start()
59-
.then(() => server.request('/foo-bar-baz/non-existent'))
60-
.catch((result) => {
61-
expect(result.statusCode).to.equal(404);
62-
});
63-
});
64-
65-
it("returns a 500 error if an error occurs", function() {
66-
let middleware = fastbootMiddleware({
67-
distPath: fixture('rejected-promise')
68-
});
69-
server = new TestHTTPServer(middleware);
70-
71-
return server.start()
72-
.then(() => server.request('/'))
73-
.catch(err => {
74-
expect(err.message).to.match(/Rejected on purpose/);
75-
});
76-
});
77-
7854
it("can be provided with a custom FastBoot instance", function() {
7955
let fastboot = new FastBoot({
8056
distPath: fixture('basic-app')
@@ -130,102 +106,156 @@ describe("FastBoot", function() {
130106
}
131107
});
132108

133-
describe('when reslient mode is enabled', function () {
134-
it("renders no FastBoot markup", function() {
135-
let middleware = fastbootMiddleware({
136-
distPath: fixture('rejected-promise'),
137-
resilient: true
138-
});
139-
server = new TestHTTPServer(middleware);
109+
[true, false].forEach((chunkedResponse) => {
110+
describe(`when chunked response is ${chunkedResponse ? 'enabled' : 'disabled'}`, function() {
111+
if (chunkedResponse) {
112+
it("responds with a chunked response", function() {
113+
let middleware = fastbootMiddleware({
114+
distPath: fixture('basic-app'),
115+
chunkedResponse
116+
});
117+
server = new TestHTTPServer(middleware, { errorHandling: true });
118+
119+
return server.start()
120+
.then(() => server.request('/', { resolveWithFullResponse: true }))
121+
.then(({ body, _, headers }) => {
122+
expect(headers['transfer-encoding']).to.eq('chunked');
123+
expect(body).to.match(/Welcome to Ember/);
124+
});
125+
});
126+
}
140127

141-
return server.start()
142-
.then(() => server.request('/'))
143-
.then(html => {
144-
expect(html).to.not.match(/error/);
128+
it("returns 404 when navigating to a URL that doesn't exist", function() {
129+
let middleware = fastbootMiddleware({
130+
distPath: fixture('basic-app'),
131+
chunkedResponse
145132
});
146-
});
133+
server = new TestHTTPServer(middleware);
147134

148-
it("propagates to error handling middleware", function() {
149-
let middleware = fastbootMiddleware({
150-
distPath: fixture('rejected-promise'),
151-
resilient: true
135+
return server.start()
136+
.then(() => server.request('/foo-bar-baz/non-existent'))
137+
.catch((result) => {
138+
expect(result.statusCode).to.equal(404);
139+
});
152140
});
153-
server = new TestHTTPServer(middleware, { errorHandling: true });
154-
155-
return server.start()
156-
.then(() => server.request('/', { resolveWithFullResponse: true }))
157-
.then(({ body, statusCode, headers }) => {
158-
expect(statusCode).to.equal(200);
159-
expect(headers['x-test-error']).to.match(/error handler called/);
160-
expect(body).to.match(/hello world/);
141+
142+
it("returns a 500 error if an error occurs", function() {
143+
let middleware = fastbootMiddleware({
144+
distPath: fixture('rejected-promise'),
145+
chunkedResponse
161146
});
162-
});
147+
server = new TestHTTPServer(middleware);
163148

164-
it("is does not propagate errors when and there is no error handling middleware", function() {
165-
let middleware = fastbootMiddleware({
166-
distPath: fixture('rejected-promise'),
167-
resilient: true,
149+
return server.start()
150+
.then(() => server.request('/'))
151+
.catch(err => {
152+
expect(err.message).to.match(/Rejected on purpose/);
153+
});
168154
});
169-
server = new TestHTTPServer(middleware, { errorHandling: false });
170-
171-
return server.start()
172-
.then(() => server.request('/', { resolveWithFullResponse: true }))
173-
.then(({ body, statusCode, headers }) => {
174-
expect(statusCode).to.equal(200);
175-
expect(headers['x-test-error']).to.not.match(/error handler called/);
176-
expect(body).to.not.match(/error/);
177-
expect(body).to.match(/hello world/);
155+
156+
describe('when reslient mode is enabled', function () {
157+
it("renders no FastBoot markup", function() {
158+
let middleware = fastbootMiddleware({
159+
distPath: fixture('rejected-promise'),
160+
resilient: true,
161+
chunkedResponse
162+
});
163+
server = new TestHTTPServer(middleware);
164+
165+
return server.start()
166+
.then(() => server.request('/'))
167+
.then(html => {
168+
expect(html).to.not.match(/error/);
169+
});
178170
});
179-
});
180171

181-
it("allows post-fastboot middleware to recover the response when it fails", function() {
182-
let middleware = fastbootMiddleware({
183-
distPath: fixture('rejected-promise'),
184-
resilient: true
185-
});
186-
server = new TestHTTPServer(middleware, { recoverErrors: true });
187-
188-
return server.start()
189-
.then(() => server.request('/', { resolveWithFullResponse: true }))
190-
.then(({ body, statusCode, headers }) => {
191-
expect(statusCode).to.equal(200);
192-
expect(headers['x-test-recovery']).to.match(/recovered response/);
193-
expect(body).to.match(/hello world/);
172+
it("propagates to error handling middleware", function() {
173+
let middleware = fastbootMiddleware({
174+
distPath: fixture('rejected-promise'),
175+
resilient: true,
176+
chunkedResponse
177+
});
178+
server = new TestHTTPServer(middleware, { errorHandling: true });
179+
180+
return server.start()
181+
.then(() => server.request('/', { resolveWithFullResponse: true }))
182+
.then(({ body, statusCode, headers }) => {
183+
expect(statusCode).to.equal(200);
184+
expect(headers['x-test-error']).to.match(/error handler called/);
185+
expect(body).to.match(/hello world/);
186+
});
187+
});
188+
189+
it("is does not propagate errors when and there is no error handling middleware", function() {
190+
let middleware = fastbootMiddleware({
191+
distPath: fixture('rejected-promise'),
192+
resilient: true,
193+
chunkedResponse
194+
});
195+
server = new TestHTTPServer(middleware, { errorHandling: false });
196+
197+
return server.start()
198+
.then(() => server.request('/', { resolveWithFullResponse: true }))
199+
.then(({ body, statusCode, headers }) => {
200+
expect(statusCode).to.equal(200);
201+
expect(headers['x-test-error']).to.not.match(/error handler called/);
202+
expect(body).to.not.match(/error/);
203+
expect(body).to.match(/hello world/);
204+
});
194205
});
195-
});
196-
});
197206

198-
describe('when reslient mode is disabled', function () {
199-
it("propagates to error handling middleware", function() {
200-
let middleware = fastbootMiddleware({
201-
distPath: fixture('rejected-promise'),
202-
resilient: false,
207+
it("allows post-fastboot middleware to recover the response when it fails", function() {
208+
let middleware = fastbootMiddleware({
209+
distPath: fixture('rejected-promise'),
210+
resilient: true,
211+
chunkedResponse
212+
});
213+
server = new TestHTTPServer(middleware, { recoverErrors: true });
214+
215+
return server.start()
216+
.then(() => server.request('/', { resolveWithFullResponse: true }))
217+
.then(({ body, statusCode, headers }) => {
218+
expect(statusCode).to.equal(200);
219+
expect(headers['x-test-recovery']).to.match(/recovered response/);
220+
expect(body).to.match(/hello world/);
221+
});
222+
});
203223
});
204-
server = new TestHTTPServer(middleware, { errorHandling: true });
205224

206-
return server.start()
207-
.then(() => server.request('/', { resolveWithFullResponse: true }))
208-
.catch(({ statusCode, response: { headers } }) => {
209-
expect(statusCode).to.equal(500);
210-
expect(headers['x-test-error']).to.match(/error handler called/);
225+
describe('when reslient mode is disabled', function () {
226+
it("propagates to error handling middleware", function() {
227+
let middleware = fastbootMiddleware({
228+
distPath: fixture('rejected-promise'),
229+
resilient: false,
230+
chunkedResponse
231+
});
232+
server = new TestHTTPServer(middleware, { errorHandling: true });
233+
234+
return server.start()
235+
.then(() => server.request('/', { resolveWithFullResponse: true }))
236+
.catch(({ statusCode, response: { headers } }) => {
237+
expect(statusCode).to.equal(500);
238+
expect(headers['x-test-error']).to.match(/error handler called/);
239+
});
211240
});
212-
});
213241

214-
it("allows post-fastboot middleware to recover the response when it fails", function() {
215-
let middleware = fastbootMiddleware({
216-
distPath: fixture('rejected-promise'),
217-
resilient: false
218-
});
219-
server = new TestHTTPServer(middleware, { recoverErrors: true });
220-
221-
return server.start()
222-
.then(() => server.request('/', { resolveWithFullResponse: true }))
223-
.then(({ body, statusCode, headers }) => {
224-
expect(statusCode).to.equal(200);
225-
expect(headers['x-test-recovery']).to.match(/recovered response/);
226-
expect(body).to.match(/hello world/);
242+
it("allows post-fastboot middleware to recover the response when it fails", function() {
243+
let middleware = fastbootMiddleware({
244+
distPath: fixture('rejected-promise'),
245+
resilient: false,
246+
chunkedResponse
247+
});
248+
server = new TestHTTPServer(middleware, { recoverErrors: true });
249+
250+
return server.start()
251+
.then(() => server.request('/', { resolveWithFullResponse: true }))
252+
.then(({ body, statusCode, headers }) => {
253+
expect(statusCode).to.equal(200);
254+
expect(headers['x-test-recovery']).to.match(/recovered response/);
255+
expect(body).to.match(/hello world/);
256+
});
227257
});
258+
});
228259
});
229260
});
230-
231261
});

0 commit comments

Comments
 (0)