@eggjs/koa 2.17.0 → 2.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,473 @@
1
+ import assert from 'node:assert';
2
+ import { extname } from 'node:path';
3
+ import util from 'node:util';
4
+ import Stream from 'node:stream';
5
+ import type { IncomingMessage, ServerResponse } from 'node:http';
6
+ import contentDisposition from 'content-disposition';
7
+ import getType from 'cache-content-type';
8
+ import onFinish from 'on-finished';
9
+ import escape from 'escape-html';
10
+ import { is as typeis } from 'type-is';
11
+ import statuses from 'statuses';
12
+ import destroy from 'destroy';
13
+ import vary from 'vary';
14
+ import encodeUrl from 'encodeurl';
15
+ import type Application from './application.js';
16
+ import type { ContextDelegation } from './context.js';
17
+ import type Request from './request.js';
18
+
19
+ export default class Response {
20
+ app: Application;
21
+ req: IncomingMessage;
22
+ res: ServerResponse;
23
+ ctx: ContextDelegation;
24
+ request: Request;
25
+
26
+ constructor(app: Application, ctx: ContextDelegation, req: IncomingMessage, res: ServerResponse) {
27
+ this.app = app;
28
+ this.req = req;
29
+ this.res = res;
30
+ this.ctx = ctx;
31
+ }
32
+
33
+ /**
34
+ * Return the request socket.
35
+ */
36
+ get socket() {
37
+ return this.res.socket;
38
+ }
39
+
40
+ /**
41
+ * Return response header.
42
+ */
43
+ get header() {
44
+ return this.res.getHeaders() || {};
45
+ }
46
+
47
+ /**
48
+ * Return response header, alias as response.header
49
+ */
50
+ get headers() {
51
+ return this.header;
52
+ }
53
+
54
+ _explicitStatus: boolean;
55
+
56
+ /**
57
+ * Get response status code.
58
+ */
59
+ get status() {
60
+ return this.res.statusCode;
61
+ }
62
+
63
+ /**
64
+ * Set response status code.
65
+ */
66
+ set status(code: number) {
67
+ if (this.headerSent) return;
68
+ assert(Number.isInteger(code), 'status code must be a number');
69
+ assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
70
+ this._explicitStatus = true;
71
+ this.res.statusCode = code;
72
+ if (this.req.httpVersionMajor < 2) {
73
+ this.res.statusMessage = statuses.message[code]!;
74
+ }
75
+ if (this.body && statuses.empty[code]) this.body = null;
76
+ }
77
+
78
+ /**
79
+ * Get response status message
80
+ */
81
+ get message(): string {
82
+ return this.res.statusMessage || statuses.message[this.status]!;
83
+ }
84
+
85
+ /**
86
+ * Set response status message
87
+ */
88
+ set message(msg: string) {
89
+ this.res.statusMessage = msg;
90
+ }
91
+
92
+ _body: any;
93
+ _explicitNullBody: boolean;
94
+
95
+ /**
96
+ * Get response body.
97
+ */
98
+ get body() {
99
+ return this._body;
100
+ }
101
+
102
+ /**
103
+ * Set response body.
104
+ */
105
+ set body(val: string | Buffer | object | Stream | null | undefined | boolean) {
106
+ const original = this._body;
107
+ this._body = val;
108
+
109
+ // no content
110
+ if (val == null) {
111
+ if (!statuses.empty[this.status]) this.status = 204;
112
+ if (val === null) this._explicitNullBody = true;
113
+ this.remove('Content-Type');
114
+ this.remove('Content-Length');
115
+ this.remove('Transfer-Encoding');
116
+ return;
117
+ }
118
+
119
+ // set the status
120
+ if (!this._explicitStatus) this.status = 200;
121
+
122
+ // set the content-type only if not yet set
123
+ const setType = !this.has('Content-Type');
124
+
125
+ // string
126
+ if (typeof val === 'string') {
127
+ if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
128
+ this.length = Buffer.byteLength(val);
129
+ return;
130
+ }
131
+
132
+ // buffer
133
+ if (Buffer.isBuffer(val)) {
134
+ if (setType) this.type = 'bin';
135
+ this.length = val.length;
136
+ return;
137
+ }
138
+
139
+ // stream
140
+ if (val instanceof Stream) {
141
+ onFinish(this.res, destroy.bind(null, val));
142
+ // eslint-disable-next-line eqeqeq
143
+ if (original != val) {
144
+ val.once('error', err => this.ctx.onerror(err));
145
+ // overwriting
146
+ if (original != null) this.remove('Content-Length');
147
+ }
148
+
149
+ if (setType) this.type = 'bin';
150
+ return;
151
+ }
152
+
153
+ // json
154
+ this.remove('Content-Length');
155
+ this.type = 'json';
156
+ }
157
+
158
+ /**
159
+ * Set Content-Length field to `n`.
160
+ */
161
+ set length(n: number | string | undefined) {
162
+ if (n === undefined) return;
163
+ if (!this.has('Transfer-Encoding')) {
164
+ this.set('Content-Length', n);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Return parsed response Content-Length when present.
170
+ */
171
+ get length() {
172
+ if (this.has('Content-Length')) {
173
+ return parseInt(this.get('Content-Length'), 10) || 0;
174
+ }
175
+
176
+ const { body } = this;
177
+ if (!body || body instanceof Stream) return undefined;
178
+ if (typeof body === 'string') return Buffer.byteLength(body);
179
+ if (Buffer.isBuffer(body)) return body.length;
180
+ return Buffer.byteLength(JSON.stringify(body));
181
+ }
182
+
183
+ /**
184
+ * Check if a header has been written to the socket.
185
+ */
186
+ get headerSent() {
187
+ return this.res.headersSent;
188
+ }
189
+
190
+ /**
191
+ * Vary on `field`.
192
+ */
193
+ vary(field: string) {
194
+ if (this.headerSent) return;
195
+ vary(this.res, field);
196
+ }
197
+
198
+ /**
199
+ * Perform a 302 redirect to `url`.
200
+ *
201
+ * The string "back" is special-cased
202
+ * to provide Referrer support, when Referrer
203
+ * is not present `alt` or "/" is used.
204
+ *
205
+ * Examples:
206
+ *
207
+ * this.redirect('back');
208
+ * this.redirect('back', '/index.html');
209
+ * this.redirect('/login');
210
+ * this.redirect('http://google.com'); // will format to 'http://google.com/'
211
+ */
212
+ redirect(url: string, alt?: string) {
213
+ // location
214
+ if (url === 'back') url = this.ctx.get<string>('Referrer') || alt || '/';
215
+ if (url.startsWith('https://') || url.startsWith('http://')) {
216
+ // formatting url again avoid security escapes
217
+ url = new URL(url).toString();
218
+ }
219
+ this.set('Location', encodeUrl(url));
220
+
221
+ // status
222
+ if (!statuses.redirect[this.status]) this.status = 302;
223
+
224
+ // html
225
+ if (this.ctx.accepts('html')) {
226
+ url = escape(url);
227
+ this.type = 'text/html; charset=utf-8';
228
+ this.body = `Redirecting to <a href="${url}">${url}</a>.`;
229
+ return;
230
+ }
231
+
232
+ // text
233
+ this.type = 'text/plain; charset=utf-8';
234
+ this.body = `Redirecting to ${url}.`;
235
+ }
236
+
237
+ /**
238
+ * Set Content-Disposition header to "attachment" with optional `filename`.
239
+ */
240
+ attachment(filename?: string, options?: any) {
241
+ if (filename) this.type = extname(filename);
242
+ this.set('Content-Disposition', contentDisposition(filename, options));
243
+ }
244
+
245
+ /**
246
+ * Set Content-Type response header with `type` through `mime.lookup()`
247
+ * when it does not contain a charset.
248
+ *
249
+ * Examples:
250
+ *
251
+ * this.type = '.html';
252
+ * this.type = 'html';
253
+ * this.type = 'json';
254
+ * this.type = 'application/json';
255
+ * this.type = 'png';
256
+ */
257
+ set type(type: string | null | undefined) {
258
+ if (!type) {
259
+ this.remove('Content-Type');
260
+ return;
261
+ }
262
+ const mimeType = getType(type);
263
+ if (mimeType) {
264
+ this.set('Content-Type', mimeType);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Return the response mime type void of
270
+ * parameters such as "charset".
271
+ */
272
+ get type() {
273
+ const type = this.get<string>('Content-Type');
274
+ if (!type) return '';
275
+ return type.split(';', 1)[0];
276
+ }
277
+
278
+ /**
279
+ * Check whether the response is one of the listed types.
280
+ * Pretty much the same as `this.request.is()`.
281
+ *
282
+ * this.response.is('html')
283
+ * this.response.is('html', 'json')
284
+ */
285
+ is(type?: string | string[], ...types: string[]): string | false {
286
+ const testTypes: string[] = Array.isArray(type) ? type :
287
+ (type ? [ type ] : []);
288
+ return typeis(this.type as string, [ ...testTypes, ...types ]);
289
+ }
290
+
291
+ /**
292
+ * Set the Last-Modified date using a string or a Date.
293
+ *
294
+ * this.response.lastModified = new Date();
295
+ * this.response.lastModified = '2013-09-13';
296
+ */
297
+ set lastModified(val: string | Date | undefined) {
298
+ if (typeof val === 'string') val = new Date(val);
299
+ if (val) {
300
+ this.set('Last-Modified', val.toUTCString());
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Get the Last-Modified date in Date form, if it exists.
306
+ */
307
+ get lastModified(): Date | undefined {
308
+ const date = this.get<string>('last-modified');
309
+ if (date) return new Date(date);
310
+ }
311
+
312
+ /**
313
+ * Set the ETag of a response.
314
+ * This will normalize the quotes if necessary.
315
+ *
316
+ * this.response.etag = 'md5-hash-sum';
317
+ * this.response.etag = '"md5-hash-sum"';
318
+ * this.response.etag = 'W/"123456789"';
319
+ */
320
+ set etag(val: string) {
321
+ if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
322
+ this.set('ETag', val);
323
+ }
324
+
325
+ /**
326
+ * Get the ETag of a response.
327
+ */
328
+ get etag() {
329
+ return this.get('ETag');
330
+ }
331
+
332
+ /**
333
+ * Return response header.
334
+ *
335
+ * Examples:
336
+ *
337
+ * this.get('Content-Type');
338
+ * // => "text/plain"
339
+ *
340
+ * this.get('content-type');
341
+ * // => "text/plain"
342
+ */
343
+ get<T = string | string[] | number>(field: string): T {
344
+ return (this.header[field.toLowerCase()] || '') as T;
345
+ }
346
+
347
+ /**
348
+ * Returns true if the header identified by name is currently set in the outgoing headers.
349
+ * The header name matching is case-insensitive.
350
+ *
351
+ * Examples:
352
+ *
353
+ * this.has('Content-Type');
354
+ * // => true
355
+ *
356
+ * this.get('content-type');
357
+ * // => true
358
+ */
359
+ has(field: string) {
360
+ return this.res.hasHeader(field);
361
+ }
362
+
363
+ /**
364
+ * Set header `field` to `val` or pass
365
+ * an object of header fields.
366
+ *
367
+ * Examples:
368
+ *
369
+ * this.set('Foo', ['bar', 'baz']);
370
+ * this.set('Accept', 'application/json');
371
+ * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
372
+ */
373
+ set(field: string | Record<string, string>, val?: string | number | any[]) {
374
+ if (this.headerSent) return;
375
+ if (typeof field === 'string') {
376
+ if (Array.isArray(val)) {
377
+ val = val.map(v => {
378
+ return typeof v === 'string' ? v : String(v);
379
+ });
380
+ } else if (typeof val !== 'string') {
381
+ val = String(val);
382
+ }
383
+ this.res.setHeader(field, val);
384
+ } else {
385
+ for (const key in field) {
386
+ this.set(key, field[key]);
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Append additional header `field` with value `val`.
393
+ *
394
+ * Examples:
395
+ *
396
+ * ```
397
+ * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
398
+ * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
399
+ * this.append('Warning', '199 Miscellaneous warning');
400
+ */
401
+ append(field: string, val: string | string[]) {
402
+ const prev = this.get(field);
403
+
404
+ let value: any | any[] = val;
405
+ if (prev) {
406
+ value = Array.isArray(prev)
407
+ ? prev.concat(value)
408
+ : [ prev ].concat(val);
409
+ }
410
+
411
+ return this.set(field, value);
412
+ }
413
+
414
+ /**
415
+ * Remove header `field`.
416
+ */
417
+ remove(field: string) {
418
+ if (this.headerSent) return;
419
+ this.res.removeHeader(field);
420
+ }
421
+
422
+ /**
423
+ * Checks if the request is writable.
424
+ * Tests for the existence of the socket
425
+ * as node sometimes does not set it.
426
+ */
427
+ get writable() {
428
+ // can't write any more after response finished
429
+ // response.writableEnded is available since Node > 12.9
430
+ // https://nodejs.org/api/http.html#http_response_writableended
431
+ // response.finished is undocumented feature of previous Node versions
432
+ // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js
433
+ if (this.res.writableEnded || this.res.finished) return false;
434
+
435
+ const socket = this.res.socket;
436
+ // There are already pending outgoing res, but still writable
437
+ // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
438
+ if (!socket) return true;
439
+ return socket.writable;
440
+ }
441
+
442
+ /**
443
+ * Inspect implementation.
444
+ */
445
+ inspect() {
446
+ if (!this.res) return;
447
+ const o = this.toJSON();
448
+ Reflect.set(o, 'body', this.body);
449
+ return o;
450
+ }
451
+
452
+ [util.inspect.custom]() {
453
+ return this.inspect();
454
+ }
455
+
456
+ /**
457
+ * Return JSON representation.
458
+ */
459
+ toJSON() {
460
+ return {
461
+ status: this.status,
462
+ message: this.message,
463
+ header: this.header,
464
+ };
465
+ }
466
+
467
+ /**
468
+ * Flush any set headers and begin the body
469
+ */
470
+ flushHeaders() {
471
+ this.res.flushHeaders();
472
+ }
473
+ }
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type CustomError = Error & {
2
+ headers?: Record<string, string>;
3
+ status?: number;
4
+ statusCode?: number;
5
+ code?: string;
6
+ expose?: boolean;
7
+ };
8
+
9
+ export type AnyProto = {
10
+ [key: string]: any;
11
+ };