UNPKG

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