UNPKG

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