UNPKG

10.4 kBJavaScriptView Raw
1'use strict';
2
3const debug = require('debug')('grown:conn:response');
4
5const statusCodes = require('http').STATUS_CODES;
6const qs = require('querystring');
7const url = require('url');
8const mime = require('mime');
9const send = require('send');
10const path = require('path');
11const https = require('https');
12const http = require('http');
13const fs = require('fs');
14
15module.exports = (Grown, util) => {
16 function _finishRequest(ctx, body) {
17 return ctx.halt(() => {
18 /* istanbul ignore else */
19 if (ctx.res.finished) {
20 throw new Error('Already finished');
21 }
22
23 ctx.res.statusCode = ctx.status_code;
24 ctx.res.statusMessage = statusCodes[ctx.res.statusCode];
25
26 /* istanbul ignore else */
27 if (body && typeof body.pipe === 'function') {
28 debug('#%s Done. Reponse is an stream. Sending as %s', ctx.pid, ctx.content_type);
29
30 /* istanbul ignore else */
31 if (!ctx.res._header) {
32 ctx.res.setHeader('Content-Type', ctx.content_type);
33 ctx.res.writeHead(ctx.res.statusCode);
34 }
35
36 return new Promise((resolve, reject) => {
37 body.on('close', resolve);
38 body.on('error', reject);
39 body.pipe(ctx.res);
40 });
41 }
42
43 /* istanbul ignore else */
44 if (body !== null && Buffer.isBuffer(body)) {
45 debug('#%s Response is a buffer. Sending as %s', ctx.pid, ctx.content_type);
46
47 /* istanbul ignore else */
48 if (!ctx.res._header) {
49 ctx.res.setHeader('Content-Type', ctx.content_type);
50 ctx.res.setHeader('Content-Length', body.length);
51 }
52 } else if (body !== null && typeof body === 'object') {
53 debug('#%s Response is an object. Sending as application/json', ctx.pid);
54
55 body = JSON.stringify(body);
56 ctx.content_type = 'application/json';
57 }
58
59 /* istanbul ignore else */
60 if (!ctx.res._header) {
61 ctx.res.setHeader('Content-Type', `${ctx.content_type}; charset=${ctx.resp_charset}`);
62 ctx.res.setHeader('Content-Length', Buffer.byteLength(body || ''));
63 ctx.res.writeHead(ctx.res.statusCode);
64 }
65
66 ctx.res.write(body || '');
67 ctx.res.end();
68 });
69 }
70
71 function _endRequest(ctx, code, message) {
72 /* istanbul ignore else */
73 if (ctx.res && ctx.res.finished) {
74 throw new Error('Already finished');
75 }
76
77 let _code = code;
78
79 /* istanbul ignore else */
80 if (typeof code === 'string' || code instanceof Buffer) {
81 _code = ctx.status_code;
82 message = code;
83 }
84
85 /* istanbul ignore else */
86 if (code instanceof Error) {
87 message = code.message || code.toString();
88 _code = code.statusCode || 500;
89 }
90
91 /* istanbul ignore else */
92 if (!ctx.has_body) {
93 ctx.resp_body = message || ctx.resp_body;
94 }
95
96 /* istanbul ignore else */
97 if (!ctx.has_status) {
98 ctx.status_code = typeof _code === 'number' ? _code : ctx.status_code;
99 }
100
101 return ctx.send(ctx.resp_body);
102 }
103
104 function _cutBody(value) {
105 /* istanbul ignore else */
106 if (typeof value !== 'string') {
107 value = util.inspect(value);
108 }
109
110 value = value.replace(/\s+/g, ' ').trim();
111
112 return value.length > 99
113 ? `${value.substr(0, 100)}...`
114 : value;
115 }
116
117 function _fixURL(location) {
118 const _uri = url.parse(location);
119
120 let _query = '';
121
122 /* istanbul ignore else */
123 if (_uri.query) {
124 _query = qs.stringify(qs.parse(_uri.query));
125 }
126
127 return [
128 _uri.protocol ? `${_uri.protocol}//` : '',
129 _uri.hostname ? _uri.hostname : '',
130 _uri.port ? `:${_uri.port}` : '',
131 _uri.pathname && _uri.pathname !== '/' ? _uri.pathname : '',
132 _query ? `?${_query}` : '',
133 ].join('');
134 }
135
136 return Grown('Conn.Response', {
137 _finishRequest,
138 _endRequest,
139 _cutBody,
140 _fixURL,
141
142 $before_render(ctx, template) {
143 util.extendValues(template.locals, ctx.state);
144 },
145
146 $mixins() {
147 const self = this;
148
149 const _response = {
150 headers: Object.create(null),
151 type: 'text/html',
152 body: null,
153 status: null,
154 charset: 'utf8',
155 };
156
157 return {
158 props: {
159 // response body
160 has_body: () => _response.body !== null,
161 has_status: () => _response.status !== null,
162
163 get content_type() {
164 return _response.type;
165 },
166
167 set content_type(mimeType) {
168 /* istanbul ignore else */
169 if (!(mimeType && typeof mimeType === 'string')) {
170 throw new Error(`Invalid type: '${mimeType}'`);
171 }
172
173 _response.type = mimeType;
174 },
175
176 get status_code() {
177 return _response.status !== null
178 ? _response.status
179 : 200;
180 },
181
182 set status_code(code) {
183 /* istanbul ignore else */
184 if (!(code && typeof code === 'number' && statusCodes[code])) {
185 throw new Error(`Invalid status_code: ${code}`);
186 }
187
188 debug('#%s Set status: %s', this.pid, code);
189
190 _response.status = code;
191 },
192
193 get resp_body() {
194 return _response.body;
195 },
196
197 set resp_body(value) {
198 /* istanbul ignore else */
199 if (!(typeof value === 'string' || (typeof value === 'object' && !Array.isArray(value))
200 || (value && typeof value.pipe === 'function') || (value instanceof Buffer))) {
201 throw new Error(`Invalid resp_body: ${value}`);
202 }
203
204 debug('#%s Set body: %s', this.pid, _cutBody(value));
205
206 _response.body = value;
207 },
208
209 get resp_charset() {
210 return _response.charset;
211 },
212
213 set resp_charset(value) {
214 /* istanbul ignore else */
215 if (typeof value !== 'string') {
216 throw new Error(`Invalid charset: ${value}`);
217 }
218
219 _response.charset = value || 'utf8';
220 },
221
222 get resp_headers() {
223 return this.res.getHeaders();
224 },
225
226 set resp_headers(value) {
227 /* istanbul ignore else */
228 if (Object.prototype.toString.call(value) !== '[object Object]') {
229 throw new Error(`Invalid headers: ${value}`);
230 }
231
232 this.res._headers = value;
233 },
234 },
235 methods: {
236 // response headers
237 get_resp_header(name) {
238 /* istanbul ignore else */
239 if (!(name && typeof name === 'string')) {
240 throw new Error(`Invalid resp_header: '${name}'`);
241 }
242
243 return this.res.getHeader(name);
244 },
245
246 put_resp_header(name, value) {
247 /* istanbul ignore else */
248 if (!name || typeof name !== 'string') {
249 throw new Error(`Invalid resp_header: '${name}' => '${value}'`);
250 }
251
252 this.res.setHeader(name, value);
253
254 return this;
255 },
256
257 merge_resp_headers(headers) {
258 /* istanbul ignore else */
259 if (!(headers && (typeof headers === 'object' && !Array.isArray(headers)))) {
260 throw new Error(`Invalid resp_headers: '${headers}'`);
261 }
262
263 Object.keys(headers).forEach(key => {
264 this.put_resp_header(key, headers[key]);
265 });
266
267 return this;
268 },
269
270 delete_resp_header(name) {
271 /* istanbul ignore else */
272 if (!(name && typeof name === 'string')) {
273 throw new Error(`Invalid resp_header: '${name}'`);
274 }
275
276 this.res.removeHeader(name);
277
278 return this;
279 },
280
281 redirect(location, timeout, body) {
282 /* istanbul ignore else */
283 if (!(location && typeof location === 'string')) {
284 throw new Error(`Invalid location: '${location}`);
285 }
286
287 /* istanbul ignore else */
288 if (timeout) {
289 const meta = `<meta http-equiv="refresh" content="${timeout};url=${location}">${body || ''}`;
290
291 return this.end(302, meta);
292 }
293
294 debug('#%s Done. Redirection was found', this.pid);
295
296 return this.put_resp_header('Location', self._fixURL(location)).end(302);
297 },
298
299 json(value) {
300 /* istanbul ignore else */
301 if (!value || typeof value !== 'object') {
302 throw new Error(`Invalid JSON value: ${value}`);
303 }
304
305 return this.send(value);
306 },
307
308 get_file(_url, filePath) {
309 return new Promise((resolve, reject) => {
310 const dest = path.resolve(filePath);
311 const file = fs.createWriteStream(dest);
312
313 (_url.indexOf('https:') !== -1 ? https : http)
314 .get(_url, response => {
315 response.pipe(file);
316 file.on('finish', () => file.close(() => resolve(dest)));
317 }).on('error', err => {
318 fs.unlink(dest);
319 reject(err);
320 });
321 });
322 },
323
324 send_file(entry, mimeType) {
325 /* istanbul ignore else */
326 if (typeof entry === 'object') {
327 mimeType = entry.type || mimeType;
328 entry = entry.file;
329 }
330
331 /* istanbul ignore else */
332 if (!mimeType) {
333 mimeType = mime.getType(entry);
334 }
335
336 const pathname = encodeURI(path.basename(entry));
337
338 const file = send(this.req, pathname, {
339 root: path.dirname(entry),
340 });
341
342 file.on('headers', _res => {
343 /* istanbul ignore else */
344 if (mimeType) {
345 _res.setHeader('Content-Type', mimeType);
346 }
347 });
348
349 return new Promise((resolve, reject) => {
350 this.res.statusCode = 200;
351 file.on('error', reject);
352 file.on('end', resolve);
353 file.pipe(this.res);
354 });
355 },
356
357 send(body) {
358 return self._finishRequest(this, body);
359 },
360
361 end(code, message) {
362 return self._endRequest(this, code, message);
363 },
364 },
365 };
366 },
367 });
368};