@eggjs/koa 3.1.0-beta.19 → 3.1.0-beta.20
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.
- package/dist/index.d.ts +917 -5
- package/dist/index.js +1363 -4
- package/package.json +2 -2
- package/dist/application.d.ts +0 -131
- package/dist/application.js +0 -233
- package/dist/context.d.ts +0 -230
- package/dist/context.js +0 -304
- package/dist/request.d.ts +0 -343
- package/dist/request.js +0 -459
- package/dist/response.d.ts +0 -231
- package/dist/response.js +0 -387
- package/dist/types.d.ts +0 -14
package/dist/index.js
CHANGED
|
@@ -1,8 +1,1367 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import util, { debuglog } from "node:util";
|
|
2
|
+
import Emitter from "node:events";
|
|
3
|
+
import Stream from "node:stream";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { getAsyncLocalStorage } from "gals";
|
|
6
|
+
import { isGeneratorFunction } from "is-type-of";
|
|
7
|
+
import onFinished from "on-finished";
|
|
8
|
+
import statuses from "statuses";
|
|
9
|
+
import compose from "koa-compose";
|
|
10
|
+
import createError, { HttpError } from "http-errors";
|
|
11
|
+
import Cookies from "cookies";
|
|
12
|
+
import net from "node:net";
|
|
13
|
+
import { format } from "node:url";
|
|
14
|
+
import qs from "node:querystring";
|
|
15
|
+
import accepts from "accepts";
|
|
16
|
+
import contentType from "content-type";
|
|
17
|
+
import parse from "parseurl";
|
|
18
|
+
import typeis, { is } from "type-is";
|
|
19
|
+
import fresh from "fresh";
|
|
20
|
+
import assert from "node:assert";
|
|
21
|
+
import { extname } from "node:path";
|
|
22
|
+
import contentDisposition from "content-disposition";
|
|
23
|
+
import { getType } from "cache-content-type";
|
|
24
|
+
import escape from "escape-html";
|
|
25
|
+
import destroy from "destroy";
|
|
26
|
+
import vary from "vary";
|
|
27
|
+
import encodeUrl from "encodeurl";
|
|
5
28
|
|
|
29
|
+
//#region src/context.ts
|
|
30
|
+
var Context = class {
|
|
31
|
+
app;
|
|
32
|
+
req;
|
|
33
|
+
res;
|
|
34
|
+
request;
|
|
35
|
+
response;
|
|
36
|
+
originalUrl;
|
|
37
|
+
respond;
|
|
38
|
+
#state = {};
|
|
39
|
+
constructor(app, req, res) {
|
|
40
|
+
this.app = app;
|
|
41
|
+
this.req = req;
|
|
42
|
+
this.res = res;
|
|
43
|
+
this.request = new app.RequestClass(app, this, req, res);
|
|
44
|
+
this.response = new app.ResponseClass(app, this, req, res);
|
|
45
|
+
this.request.response = this.response;
|
|
46
|
+
this.response.request = this.request;
|
|
47
|
+
this.originalUrl = req.url ?? "/";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* util.inspect() implementation, which
|
|
51
|
+
* just returns the JSON output.
|
|
52
|
+
*/
|
|
53
|
+
inspect() {
|
|
54
|
+
return this.toJSON();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Custom inspection implementation for newer Node.js versions.
|
|
58
|
+
*/
|
|
59
|
+
[util.inspect.custom]() {
|
|
60
|
+
return this.inspect();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Return JSON representation.
|
|
64
|
+
*
|
|
65
|
+
* Here we explicitly invoke .toJSON() on each
|
|
66
|
+
* object, as iteration will otherwise fail due
|
|
67
|
+
* to the getters and cause utilities such as
|
|
68
|
+
* clone() to fail.
|
|
69
|
+
*/
|
|
70
|
+
toJSON() {
|
|
71
|
+
return {
|
|
72
|
+
request: this.request.toJSON(),
|
|
73
|
+
response: this.response.toJSON(),
|
|
74
|
+
app: this.app.toJSON(),
|
|
75
|
+
originalUrl: this.originalUrl,
|
|
76
|
+
req: "<original node req>",
|
|
77
|
+
res: "<original node res>",
|
|
78
|
+
socket: "<original node socket>"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
assert(value, status, errorMessageOrProps, errorProps) {
|
|
82
|
+
if (value) return;
|
|
83
|
+
status = status ?? 500;
|
|
84
|
+
if (typeof errorMessageOrProps === "string") throw createError(status, errorMessageOrProps, errorProps ?? {});
|
|
85
|
+
throw createError(status, errorMessageOrProps ?? {});
|
|
86
|
+
}
|
|
87
|
+
throw(arg1, arg2, errorProps) {
|
|
88
|
+
const args = [];
|
|
89
|
+
if (typeof arg2 === "number") {
|
|
90
|
+
args.push(arg2);
|
|
91
|
+
args.push(arg1);
|
|
92
|
+
} else {
|
|
93
|
+
args.push(arg1);
|
|
94
|
+
if (arg2) args.push(arg2);
|
|
95
|
+
}
|
|
96
|
+
if (errorProps) args.push(errorProps);
|
|
97
|
+
throw createError(...args);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Default error handling.
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
onerror(err) {
|
|
104
|
+
if (err === null || err === void 0) return;
|
|
105
|
+
if (!(err instanceof Error || Object.prototype.toString.call(err) === "[object Error]")) err = new Error(util.format("non-error thrown: %j", err));
|
|
106
|
+
let headerSent = false;
|
|
107
|
+
if (this.response.headerSent || !this.response.writable) {
|
|
108
|
+
headerSent = true;
|
|
109
|
+
err.headerSent = true;
|
|
110
|
+
}
|
|
111
|
+
this.app.emit("error", err, this);
|
|
112
|
+
if (headerSent) return;
|
|
113
|
+
const res = this.res;
|
|
114
|
+
for (const name of res.getHeaderNames()) res.removeHeader(name);
|
|
115
|
+
if (err.headers) this.response.set(err.headers);
|
|
116
|
+
this.response.type = "text";
|
|
117
|
+
let statusCode = err.status || err.statusCode;
|
|
118
|
+
if (err.code === "ENOENT") statusCode = 404;
|
|
119
|
+
if (typeof statusCode !== "number" || !statuses.message[statusCode]) statusCode = 500;
|
|
120
|
+
const statusMessage = statuses.message[statusCode];
|
|
121
|
+
const msg = err.expose ? err.message : statusMessage;
|
|
122
|
+
err.status = statusCode;
|
|
123
|
+
this.response.status = statusCode;
|
|
124
|
+
this.response.length = Buffer.byteLength(msg);
|
|
125
|
+
res.end(msg);
|
|
126
|
+
}
|
|
127
|
+
_cookies;
|
|
128
|
+
get cookies() {
|
|
129
|
+
if (!this._cookies) this._cookies = new Cookies(this.req, this.res, {
|
|
130
|
+
keys: this.app.keys,
|
|
131
|
+
secure: this.request.secure
|
|
132
|
+
});
|
|
133
|
+
return this._cookies;
|
|
134
|
+
}
|
|
135
|
+
set cookies(cookies) {
|
|
136
|
+
this._cookies = cookies;
|
|
137
|
+
}
|
|
138
|
+
get state() {
|
|
139
|
+
return this.#state;
|
|
140
|
+
}
|
|
141
|
+
acceptsLanguages(languages, ...others) {
|
|
142
|
+
return this.request.acceptsLanguages(languages, ...others);
|
|
143
|
+
}
|
|
144
|
+
acceptsEncodings(encodings, ...others) {
|
|
145
|
+
return this.request.acceptsEncodings(encodings, ...others);
|
|
146
|
+
}
|
|
147
|
+
acceptsCharsets(charsets, ...others) {
|
|
148
|
+
return this.request.acceptsCharsets(charsets, ...others);
|
|
149
|
+
}
|
|
150
|
+
accepts(args, ...others) {
|
|
151
|
+
return this.request.accepts(args, ...others);
|
|
152
|
+
}
|
|
153
|
+
get(field) {
|
|
154
|
+
return this.request.get(field);
|
|
155
|
+
}
|
|
156
|
+
is(type, ...types) {
|
|
157
|
+
return this.request.is(type, ...types);
|
|
158
|
+
}
|
|
159
|
+
get querystring() {
|
|
160
|
+
return this.request.querystring;
|
|
161
|
+
}
|
|
162
|
+
set querystring(str) {
|
|
163
|
+
this.request.querystring = str;
|
|
164
|
+
}
|
|
165
|
+
get idempotent() {
|
|
166
|
+
return this.request.idempotent;
|
|
167
|
+
}
|
|
168
|
+
get socket() {
|
|
169
|
+
return this.request.socket;
|
|
170
|
+
}
|
|
171
|
+
get search() {
|
|
172
|
+
return this.request.search;
|
|
173
|
+
}
|
|
174
|
+
set search(str) {
|
|
175
|
+
this.request.search = str;
|
|
176
|
+
}
|
|
177
|
+
get method() {
|
|
178
|
+
return this.request.method;
|
|
179
|
+
}
|
|
180
|
+
set method(method) {
|
|
181
|
+
this.request.method = method;
|
|
182
|
+
}
|
|
183
|
+
get query() {
|
|
184
|
+
return this.request.query;
|
|
185
|
+
}
|
|
186
|
+
set query(obj) {
|
|
187
|
+
this.request.query = obj;
|
|
188
|
+
}
|
|
189
|
+
get path() {
|
|
190
|
+
return this.request.path;
|
|
191
|
+
}
|
|
192
|
+
set path(path) {
|
|
193
|
+
this.request.path = path;
|
|
194
|
+
}
|
|
195
|
+
get url() {
|
|
196
|
+
return this.request.url;
|
|
197
|
+
}
|
|
198
|
+
set url(url) {
|
|
199
|
+
this.request.url = url;
|
|
200
|
+
}
|
|
201
|
+
get accept() {
|
|
202
|
+
return this.request.accept;
|
|
203
|
+
}
|
|
204
|
+
set accept(accept) {
|
|
205
|
+
this.request.accept = accept;
|
|
206
|
+
}
|
|
207
|
+
get origin() {
|
|
208
|
+
return this.request.origin;
|
|
209
|
+
}
|
|
210
|
+
get href() {
|
|
211
|
+
return this.request.href;
|
|
212
|
+
}
|
|
213
|
+
get subdomains() {
|
|
214
|
+
return this.request.subdomains;
|
|
215
|
+
}
|
|
216
|
+
get protocol() {
|
|
217
|
+
return this.request.protocol;
|
|
218
|
+
}
|
|
219
|
+
get host() {
|
|
220
|
+
return this.request.host;
|
|
221
|
+
}
|
|
222
|
+
get hostname() {
|
|
223
|
+
return this.request.hostname;
|
|
224
|
+
}
|
|
225
|
+
get URL() {
|
|
226
|
+
return this.request.URL;
|
|
227
|
+
}
|
|
228
|
+
get header() {
|
|
229
|
+
return this.request.header;
|
|
230
|
+
}
|
|
231
|
+
get headers() {
|
|
232
|
+
return this.request.headers;
|
|
233
|
+
}
|
|
234
|
+
get secure() {
|
|
235
|
+
return this.request.secure;
|
|
236
|
+
}
|
|
237
|
+
get stale() {
|
|
238
|
+
return this.request.stale;
|
|
239
|
+
}
|
|
240
|
+
get fresh() {
|
|
241
|
+
return this.request.fresh;
|
|
242
|
+
}
|
|
243
|
+
get ips() {
|
|
244
|
+
return this.request.ips;
|
|
245
|
+
}
|
|
246
|
+
get ip() {
|
|
247
|
+
return this.request.ip;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Response delegation.
|
|
251
|
+
*/
|
|
252
|
+
attachment(...args) {
|
|
253
|
+
return this.response.attachment(...args);
|
|
254
|
+
}
|
|
255
|
+
redirect(...args) {
|
|
256
|
+
return this.response.redirect(...args);
|
|
257
|
+
}
|
|
258
|
+
remove(...args) {
|
|
259
|
+
return this.response.remove(...args);
|
|
260
|
+
}
|
|
261
|
+
vary(...args) {
|
|
262
|
+
return this.response.vary(...args);
|
|
263
|
+
}
|
|
264
|
+
has(...args) {
|
|
265
|
+
return this.response.has(...args);
|
|
266
|
+
}
|
|
267
|
+
set(...args) {
|
|
268
|
+
return this.response.set(...args);
|
|
269
|
+
}
|
|
270
|
+
append(...args) {
|
|
271
|
+
return this.response.append(...args);
|
|
272
|
+
}
|
|
273
|
+
flushHeaders(...args) {
|
|
274
|
+
return this.response.flushHeaders(...args);
|
|
275
|
+
}
|
|
276
|
+
get status() {
|
|
277
|
+
return this.response.status;
|
|
278
|
+
}
|
|
279
|
+
set status(status) {
|
|
280
|
+
this.response.status = status;
|
|
281
|
+
}
|
|
282
|
+
get message() {
|
|
283
|
+
return this.response.message;
|
|
284
|
+
}
|
|
285
|
+
set message(msg) {
|
|
286
|
+
this.response.message = msg;
|
|
287
|
+
}
|
|
288
|
+
get body() {
|
|
289
|
+
return this.response.body;
|
|
290
|
+
}
|
|
291
|
+
set body(val) {
|
|
292
|
+
this.response.body = val;
|
|
293
|
+
}
|
|
294
|
+
get length() {
|
|
295
|
+
return this.response.length;
|
|
296
|
+
}
|
|
297
|
+
set length(n) {
|
|
298
|
+
this.response.length = n;
|
|
299
|
+
}
|
|
300
|
+
get type() {
|
|
301
|
+
return this.response.type;
|
|
302
|
+
}
|
|
303
|
+
set type(type) {
|
|
304
|
+
this.response.type = type;
|
|
305
|
+
}
|
|
306
|
+
get lastModified() {
|
|
307
|
+
return this.response.lastModified;
|
|
308
|
+
}
|
|
309
|
+
set lastModified(val) {
|
|
310
|
+
this.response.lastModified = val;
|
|
311
|
+
}
|
|
312
|
+
get etag() {
|
|
313
|
+
return this.response.etag;
|
|
314
|
+
}
|
|
315
|
+
set etag(val) {
|
|
316
|
+
this.response.etag = val;
|
|
317
|
+
}
|
|
318
|
+
get headerSent() {
|
|
319
|
+
return this.response.headerSent;
|
|
320
|
+
}
|
|
321
|
+
get writable() {
|
|
322
|
+
return this.response.writable;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/request.ts
|
|
328
|
+
var Request = class {
|
|
329
|
+
app;
|
|
330
|
+
req;
|
|
331
|
+
res;
|
|
332
|
+
ctx;
|
|
333
|
+
response;
|
|
334
|
+
originalUrl;
|
|
335
|
+
constructor(app, ctx, req, res) {
|
|
336
|
+
this.app = app;
|
|
337
|
+
this.req = req;
|
|
338
|
+
this.res = res;
|
|
339
|
+
this.ctx = ctx;
|
|
340
|
+
this.originalUrl = req.url ?? "/";
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Return request header.
|
|
344
|
+
*/
|
|
345
|
+
get header() {
|
|
346
|
+
return this.req.headers;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Set request header.
|
|
350
|
+
*/
|
|
351
|
+
set header(val) {
|
|
352
|
+
this.req.headers = val;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Return request header, alias as request.header
|
|
356
|
+
*/
|
|
357
|
+
get headers() {
|
|
358
|
+
return this.req.headers;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Set request header, alias as request.header
|
|
362
|
+
*/
|
|
363
|
+
set headers(val) {
|
|
364
|
+
this.req.headers = val;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Get request URL.
|
|
368
|
+
*/
|
|
369
|
+
get url() {
|
|
370
|
+
return this.req.url ?? "/";
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Set request URL.
|
|
374
|
+
*/
|
|
375
|
+
set url(val) {
|
|
376
|
+
this.req.url = val;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get origin of URL.
|
|
380
|
+
*/
|
|
381
|
+
get origin() {
|
|
382
|
+
return `${this.protocol}://${this.host}`;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get full request URL.
|
|
386
|
+
*/
|
|
387
|
+
get href() {
|
|
388
|
+
if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl;
|
|
389
|
+
return this.origin + this.originalUrl;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Get request method.
|
|
393
|
+
*/
|
|
394
|
+
get method() {
|
|
395
|
+
return this.req.method ?? "GET";
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Set request method.
|
|
399
|
+
*/
|
|
400
|
+
set method(val) {
|
|
401
|
+
this.req.method = val;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Get request pathname.
|
|
405
|
+
*/
|
|
406
|
+
get path() {
|
|
407
|
+
return parse(this.req)?.pathname ?? "";
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Set pathname, retaining the query string when present.
|
|
411
|
+
*/
|
|
412
|
+
set path(pathname) {
|
|
413
|
+
const url = parse(this.req);
|
|
414
|
+
if (!url) return;
|
|
415
|
+
if (url.pathname === pathname) return;
|
|
416
|
+
url.pathname = pathname;
|
|
417
|
+
url.path = null;
|
|
418
|
+
this.url = format(url);
|
|
419
|
+
}
|
|
420
|
+
_parsedUrlQueryCache;
|
|
421
|
+
/**
|
|
422
|
+
* Get parsed query string.
|
|
423
|
+
*/
|
|
424
|
+
get query() {
|
|
425
|
+
const str = this.querystring;
|
|
426
|
+
if (!this._parsedUrlQueryCache) this._parsedUrlQueryCache = {};
|
|
427
|
+
let parsedUrlQuery = this._parsedUrlQueryCache[str];
|
|
428
|
+
if (!parsedUrlQuery) {
|
|
429
|
+
parsedUrlQuery = qs.parse(str);
|
|
430
|
+
this._parsedUrlQueryCache[str] = parsedUrlQuery;
|
|
431
|
+
}
|
|
432
|
+
return parsedUrlQuery;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Set query string as an object.
|
|
436
|
+
*/
|
|
437
|
+
set query(obj) {
|
|
438
|
+
this.querystring = qs.stringify(obj);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get query string.
|
|
442
|
+
*/
|
|
443
|
+
get querystring() {
|
|
444
|
+
if (!this.req) return "";
|
|
445
|
+
return parse(this.req)?.query ?? "";
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Set query string.
|
|
449
|
+
*/
|
|
450
|
+
set querystring(str) {
|
|
451
|
+
const url = parse(this.req);
|
|
452
|
+
if (!url) return;
|
|
453
|
+
if (url.search === `?${str}`) return;
|
|
454
|
+
url.search = str;
|
|
455
|
+
url.path = null;
|
|
456
|
+
this.url = format(url);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get the search string. Same as the query string
|
|
460
|
+
* except it includes the leading ?.
|
|
461
|
+
*/
|
|
462
|
+
get search() {
|
|
463
|
+
const querystring = this.querystring;
|
|
464
|
+
if (!querystring) return "";
|
|
465
|
+
return `?${querystring}`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Set the search string. Same as
|
|
469
|
+
* request.querystring= but included for ubiquity.
|
|
470
|
+
*/
|
|
471
|
+
set search(str) {
|
|
472
|
+
this.querystring = str;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Parse the "Host" header field host
|
|
476
|
+
* and support X-Forwarded-Host when a
|
|
477
|
+
* proxy is enabled.
|
|
478
|
+
* return `hostname:port` format
|
|
479
|
+
*/
|
|
480
|
+
get host() {
|
|
481
|
+
let host = this.app.proxy ? this.get("X-Forwarded-Host") : "";
|
|
482
|
+
if (host) host = splitCommaSeparatedValues(host, 1)[0];
|
|
483
|
+
if (!host) {
|
|
484
|
+
if (this.req.httpVersionMajor >= 2) host = this.get(":authority");
|
|
485
|
+
if (!host) host = this.get("Host");
|
|
486
|
+
}
|
|
487
|
+
return host;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Parse the "Host" header field hostname
|
|
491
|
+
* and support X-Forwarded-Host when a
|
|
492
|
+
* proxy is enabled.
|
|
493
|
+
*/
|
|
494
|
+
get hostname() {
|
|
495
|
+
const host = this.host;
|
|
496
|
+
if (!host) return "";
|
|
497
|
+
if (host[0] === "[") return this.URL.hostname || "";
|
|
498
|
+
return host.split(":", 1)[0];
|
|
499
|
+
}
|
|
500
|
+
_memoizedURL;
|
|
501
|
+
/**
|
|
502
|
+
* Get WHATWG parsed URL.
|
|
503
|
+
* Lazily memoized.
|
|
504
|
+
*/
|
|
505
|
+
get URL() {
|
|
506
|
+
if (!this._memoizedURL) {
|
|
507
|
+
const originalUrl = this.originalUrl || "";
|
|
508
|
+
try {
|
|
509
|
+
this._memoizedURL = new URL(`${this.origin}${originalUrl}`);
|
|
510
|
+
} catch {
|
|
511
|
+
this._memoizedURL = Object.create(null);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return this._memoizedURL;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Check if the request is fresh, aka
|
|
518
|
+
* Last-Modified and/or the ETag
|
|
519
|
+
* still match.
|
|
520
|
+
*/
|
|
521
|
+
get fresh() {
|
|
522
|
+
const method = this.method;
|
|
523
|
+
const status = this.response.status;
|
|
524
|
+
if (method !== "GET" && method !== "HEAD") return false;
|
|
525
|
+
if (status >= 200 && status < 300 || status === 304) return fresh(this.header, this.response.header);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if the request is stale, aka
|
|
530
|
+
* "Last-Modified" and / or the "ETag" for the
|
|
531
|
+
* resource has changed.
|
|
532
|
+
*/
|
|
533
|
+
get stale() {
|
|
534
|
+
return !this.fresh;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Check if the request is idempotent.
|
|
538
|
+
*/
|
|
539
|
+
get idempotent() {
|
|
540
|
+
return [
|
|
541
|
+
"GET",
|
|
542
|
+
"HEAD",
|
|
543
|
+
"PUT",
|
|
544
|
+
"DELETE",
|
|
545
|
+
"OPTIONS",
|
|
546
|
+
"TRACE"
|
|
547
|
+
].includes(this.method);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Return the request socket.
|
|
551
|
+
*/
|
|
552
|
+
get socket() {
|
|
553
|
+
return this.req.socket;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Get the charset when present or undefined.
|
|
557
|
+
*/
|
|
558
|
+
get charset() {
|
|
559
|
+
try {
|
|
560
|
+
const { parameters } = contentType.parse(this.req);
|
|
561
|
+
return parameters.charset || "";
|
|
562
|
+
} catch {
|
|
563
|
+
return "";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Return parsed Content-Length when present.
|
|
568
|
+
*/
|
|
569
|
+
get length() {
|
|
570
|
+
const len = this.get("Content-Length");
|
|
571
|
+
if (len === "") return;
|
|
572
|
+
return Number.parseInt(len);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Return the protocol string "http" or "https"
|
|
576
|
+
* when requested with TLS. When the proxy setting
|
|
577
|
+
* is enabled the "X-Forwarded-Proto" header
|
|
578
|
+
* field will be trusted. If you're running behind
|
|
579
|
+
* a reverse proxy that supplies https for you this
|
|
580
|
+
* may be enabled.
|
|
581
|
+
*/
|
|
582
|
+
get protocol() {
|
|
583
|
+
if (this.socket.encrypted) return "https";
|
|
584
|
+
if (!this.app.proxy) return "http";
|
|
585
|
+
let proto = this.get("X-Forwarded-Proto");
|
|
586
|
+
if (proto) proto = splitCommaSeparatedValues(proto, 1)[0];
|
|
587
|
+
return proto || "http";
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Shorthand for:
|
|
591
|
+
*
|
|
592
|
+
* this.protocol == 'https'
|
|
593
|
+
*/
|
|
594
|
+
get secure() {
|
|
595
|
+
return this.protocol === "https";
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* When `app.proxy` is `true`, parse
|
|
599
|
+
* the "X-Forwarded-For" ip address list.
|
|
600
|
+
*
|
|
601
|
+
* For example if the value was "client, proxy1, proxy2"
|
|
602
|
+
* you would receive the array `["client", "proxy1", "proxy2"]`
|
|
603
|
+
* where "proxy2" is the furthest down-stream.
|
|
604
|
+
*/
|
|
605
|
+
get ips() {
|
|
606
|
+
const proxy = this.app.proxy;
|
|
607
|
+
const val = this.get(this.app.proxyIpHeader);
|
|
608
|
+
let ips = proxy && val ? splitCommaSeparatedValues(val) : [];
|
|
609
|
+
if (this.app.maxIpsCount > 0) ips = ips.slice(-this.app.maxIpsCount);
|
|
610
|
+
return ips;
|
|
611
|
+
}
|
|
612
|
+
_ip;
|
|
613
|
+
/**
|
|
614
|
+
* Return request's remote address
|
|
615
|
+
* When `app.proxy` is `true`, parse
|
|
616
|
+
* the "X-Forwarded-For" ip address list and return the first one
|
|
617
|
+
*/
|
|
618
|
+
get ip() {
|
|
619
|
+
if (!this._ip) this._ip = this.ips[0] || this.socket.remoteAddress || "";
|
|
620
|
+
return this._ip;
|
|
621
|
+
}
|
|
622
|
+
set ip(ip) {
|
|
623
|
+
this._ip = ip;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Return subdomains as an array.
|
|
627
|
+
*
|
|
628
|
+
* Subdomains are the dot-separated parts of the host before the main domain
|
|
629
|
+
* of the app. By default, the domain of the app is assumed to be the last two
|
|
630
|
+
* parts of the host. This can be changed by setting `app.subdomainOffset`.
|
|
631
|
+
*
|
|
632
|
+
* For example, if the domain is "tobi.ferrets.example.com":
|
|
633
|
+
* If `app.subdomainOffset` is not set, this.subdomains is
|
|
634
|
+
* `["ferrets", "tobi"]`.
|
|
635
|
+
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
|
|
636
|
+
*/
|
|
637
|
+
get subdomains() {
|
|
638
|
+
const offset = this.app.subdomainOffset;
|
|
639
|
+
const hostname = this.hostname;
|
|
640
|
+
if (net.isIP(hostname)) return [];
|
|
641
|
+
return hostname.split(".").reverse().slice(offset);
|
|
642
|
+
}
|
|
643
|
+
_accept;
|
|
644
|
+
/**
|
|
645
|
+
* Get accept object.
|
|
646
|
+
* Lazily memoized.
|
|
647
|
+
*/
|
|
648
|
+
get accept() {
|
|
649
|
+
return this._accept || (this._accept = accepts(this.req));
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Set accept object.
|
|
653
|
+
*/
|
|
654
|
+
set accept(obj) {
|
|
655
|
+
this._accept = obj;
|
|
656
|
+
}
|
|
657
|
+
accepts(args, ...others) {
|
|
658
|
+
return this.accept.types(args, ...others);
|
|
659
|
+
}
|
|
660
|
+
acceptsEncodings(encodings, ...others) {
|
|
661
|
+
if (!encodings) return this.accept.encodings();
|
|
662
|
+
if (Array.isArray(encodings)) encodings = [...encodings, ...others];
|
|
663
|
+
else encodings = [encodings, ...others];
|
|
664
|
+
return this.accept.encodings(...encodings);
|
|
665
|
+
}
|
|
666
|
+
acceptsCharsets(charsets, ...others) {
|
|
667
|
+
if (!charsets) return this.accept.charsets();
|
|
668
|
+
if (Array.isArray(charsets)) charsets = [...charsets, ...others];
|
|
669
|
+
else charsets = [charsets, ...others];
|
|
670
|
+
return this.accept.charsets(...charsets);
|
|
671
|
+
}
|
|
672
|
+
acceptsLanguages(languages, ...others) {
|
|
673
|
+
if (!languages) return this.accept.languages();
|
|
674
|
+
if (Array.isArray(languages)) languages = [...languages, ...others];
|
|
675
|
+
else languages = [languages, ...others];
|
|
676
|
+
return this.accept.languages(...languages);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Check if the incoming request contains the "Content-Type"
|
|
680
|
+
* header field and if it contains any of the given mime `type`s.
|
|
681
|
+
* If there is no request body, `null` is returned.
|
|
682
|
+
* If there is no content type, `false` is returned.
|
|
683
|
+
* Otherwise, it returns the first `type` that matches.
|
|
684
|
+
*
|
|
685
|
+
* Examples:
|
|
686
|
+
*
|
|
687
|
+
* // With Content-Type: text/html; charset=utf-8
|
|
688
|
+
* this.is('html'); // => 'html'
|
|
689
|
+
* this.is('text/html'); // => 'text/html'
|
|
690
|
+
* this.is('text/*', 'application/json'); // => 'text/html'
|
|
691
|
+
*
|
|
692
|
+
* // When Content-Type is application/json
|
|
693
|
+
* this.is('json', 'urlencoded'); // => 'json'
|
|
694
|
+
* this.is('application/json'); // => 'application/json'
|
|
695
|
+
* this.is('html', 'application/*'); // => 'application/json'
|
|
696
|
+
*
|
|
697
|
+
* this.is('html'); // => false
|
|
698
|
+
*/
|
|
699
|
+
is(type, ...types) {
|
|
700
|
+
let testTypes = [];
|
|
701
|
+
if (type) testTypes = Array.isArray(type) ? type : [type];
|
|
702
|
+
return typeis(this.req, [...testTypes, ...types]);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Return the request mime type void of
|
|
706
|
+
* parameters such as "charset".
|
|
707
|
+
*/
|
|
708
|
+
get type() {
|
|
709
|
+
const type = this.get("Content-Type");
|
|
710
|
+
if (!type) return "";
|
|
711
|
+
return type.split(";")[0];
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Return request header.
|
|
715
|
+
*
|
|
716
|
+
* The `Referrer` header field is special-cased,
|
|
717
|
+
* both `Referrer` and `Referer` are interchangeable.
|
|
718
|
+
*
|
|
719
|
+
* Examples:
|
|
720
|
+
*
|
|
721
|
+
* this.get('Content-Type');
|
|
722
|
+
* // => "text/plain"
|
|
723
|
+
*
|
|
724
|
+
* this.get('content-type');
|
|
725
|
+
* // => "text/plain"
|
|
726
|
+
*
|
|
727
|
+
* this.get('Something');
|
|
728
|
+
* // => ''
|
|
729
|
+
*/
|
|
730
|
+
get(field) {
|
|
731
|
+
const req = this.req;
|
|
732
|
+
switch (field = field.toLowerCase()) {
|
|
733
|
+
case "referer":
|
|
734
|
+
case "referrer": return req.headers.referrer || req.headers.referer || "";
|
|
735
|
+
default: return req.headers[field] || "";
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Inspect implementation.
|
|
740
|
+
*/
|
|
741
|
+
inspect() {
|
|
742
|
+
if (!this.req) return;
|
|
743
|
+
return this.toJSON();
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Custom inspection implementation for newer Node.js versions.
|
|
747
|
+
*/
|
|
748
|
+
[util.inspect.custom]() {
|
|
749
|
+
return this.inspect();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Return JSON representation.
|
|
753
|
+
*/
|
|
754
|
+
toJSON() {
|
|
755
|
+
return {
|
|
756
|
+
method: this.method,
|
|
757
|
+
url: this.url,
|
|
758
|
+
header: this.header
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
/**
|
|
763
|
+
* Split a comma-separated value string into an array of values, with an optional limit.
|
|
764
|
+
* All the values are trimmed of whitespace and filtered out empty values.
|
|
765
|
+
*
|
|
766
|
+
* @param {string} value - The comma-separated value string to split.
|
|
767
|
+
* @param {number} [limit] - The maximum number of values to return.
|
|
768
|
+
* @returns {string[]} An array of values from the comma-separated string.
|
|
769
|
+
*/
|
|
770
|
+
function splitCommaSeparatedValues(value, limit) {
|
|
771
|
+
return value.split(",", limit).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/response.ts
|
|
776
|
+
var Response = class {
|
|
777
|
+
app;
|
|
778
|
+
req;
|
|
779
|
+
res;
|
|
780
|
+
ctx;
|
|
781
|
+
request;
|
|
782
|
+
constructor(app, ctx, req, res) {
|
|
783
|
+
this.app = app;
|
|
784
|
+
this.req = req;
|
|
785
|
+
this.res = res;
|
|
786
|
+
this.ctx = ctx;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Return the request socket.
|
|
790
|
+
*/
|
|
791
|
+
get socket() {
|
|
792
|
+
return this.res.socket;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Return response header.
|
|
796
|
+
*/
|
|
797
|
+
get header() {
|
|
798
|
+
return this.res.getHeaders() || {};
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Return response header, alias as response.header
|
|
802
|
+
*/
|
|
803
|
+
get headers() {
|
|
804
|
+
return this.header;
|
|
805
|
+
}
|
|
806
|
+
_explicitStatus;
|
|
807
|
+
/**
|
|
808
|
+
* Get response status code.
|
|
809
|
+
*/
|
|
810
|
+
get status() {
|
|
811
|
+
return this.res.statusCode;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Set response status code.
|
|
815
|
+
*/
|
|
816
|
+
set status(code) {
|
|
817
|
+
if (this.headerSent) return;
|
|
818
|
+
assert.ok(Number.isInteger(code), "status code must be a number");
|
|
819
|
+
assert.ok(code >= 100 && code <= 999, `invalid status code: ${code}`);
|
|
820
|
+
this._explicitStatus = true;
|
|
821
|
+
this.res.statusCode = code;
|
|
822
|
+
if (this.req.httpVersionMajor < 2 && statuses.message[code]) this.res.statusMessage = statuses.message[code];
|
|
823
|
+
if (this.body && statuses.empty[code]) this.body = null;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get response status message
|
|
827
|
+
*/
|
|
828
|
+
get message() {
|
|
829
|
+
return this.res.statusMessage ?? statuses.message[this.status];
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Set response status message
|
|
833
|
+
*/
|
|
834
|
+
set message(msg) {
|
|
835
|
+
this.res.statusMessage = msg;
|
|
836
|
+
}
|
|
837
|
+
_body;
|
|
838
|
+
_explicitNullBody;
|
|
839
|
+
/**
|
|
840
|
+
* Get response body.
|
|
841
|
+
*/
|
|
842
|
+
get body() {
|
|
843
|
+
return this._body;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Set response body.
|
|
847
|
+
*/
|
|
848
|
+
set body(val) {
|
|
849
|
+
const original = this._body;
|
|
850
|
+
this._body = val;
|
|
851
|
+
if (val === null || val === void 0) {
|
|
852
|
+
if (!statuses.empty[this.status]) this.status = 204;
|
|
853
|
+
if (val === null) this._explicitNullBody = true;
|
|
854
|
+
this.remove("Content-Type");
|
|
855
|
+
this.remove("Content-Length");
|
|
856
|
+
this.remove("Transfer-Encoding");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (!this._explicitStatus) this.status = 200;
|
|
860
|
+
const setType = !this.has("Content-Type");
|
|
861
|
+
if (typeof val === "string") {
|
|
862
|
+
if (setType) this.type = /^\s*?</.test(val) ? "html" : "text";
|
|
863
|
+
this.length = Buffer.byteLength(val);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (Buffer.isBuffer(val)) {
|
|
867
|
+
if (setType) this.type = "bin";
|
|
868
|
+
this.length = val.length;
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (val instanceof Stream) {
|
|
872
|
+
onFinished(this.res, destroy.bind(null, val));
|
|
873
|
+
if (original != val) {
|
|
874
|
+
val.once("error", (err) => this.ctx.onerror(err));
|
|
875
|
+
if (original !== null && original !== void 0) this.remove("Content-Length");
|
|
876
|
+
}
|
|
877
|
+
if (setType) this.type = "bin";
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
this.remove("Content-Length");
|
|
881
|
+
this.type = "json";
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Set Content-Length field to `n`.
|
|
885
|
+
*/
|
|
886
|
+
set length(n) {
|
|
887
|
+
if (n === void 0) return;
|
|
888
|
+
if (!this.has("Transfer-Encoding")) this.set("Content-Length", n);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Return parsed response Content-Length when present.
|
|
892
|
+
*
|
|
893
|
+
* When Content-Length is not defined it will return `undefined`.
|
|
894
|
+
*/
|
|
895
|
+
get length() {
|
|
896
|
+
if (this.has("Content-Length")) return Number.parseInt(this.get("Content-Length")) || 0;
|
|
897
|
+
const body = this.body;
|
|
898
|
+
if (!body || body instanceof Stream) return;
|
|
899
|
+
if (typeof body === "string") return Buffer.byteLength(body);
|
|
900
|
+
if (Buffer.isBuffer(body)) return body.length;
|
|
901
|
+
return Buffer.byteLength(JSON.stringify(body));
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Check if a header has been written to the socket.
|
|
905
|
+
*/
|
|
906
|
+
get headerSent() {
|
|
907
|
+
return this.res.headersSent;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Vary on `field`.
|
|
911
|
+
*/
|
|
912
|
+
vary(field) {
|
|
913
|
+
if (this.headerSent) return;
|
|
914
|
+
vary(this.res, field);
|
|
915
|
+
}
|
|
916
|
+
_getBackReferrer() {
|
|
917
|
+
const referrer = this.ctx.get("Referrer");
|
|
918
|
+
if (referrer) {
|
|
919
|
+
if (referrer.startsWith("/")) return referrer;
|
|
920
|
+
if (new URL(referrer, this.ctx.href).host === this.ctx.host) return referrer;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Perform a 302 redirect to `url`.
|
|
925
|
+
*
|
|
926
|
+
* The string "back" is special-cased
|
|
927
|
+
* to provide Referrer support, when Referrer
|
|
928
|
+
* is not present `alt` or "/" is used.
|
|
929
|
+
*
|
|
930
|
+
* Examples:
|
|
931
|
+
*
|
|
932
|
+
* this.redirect('back');
|
|
933
|
+
* this.redirect('back', '/index.html');
|
|
934
|
+
* this.redirect('/login');
|
|
935
|
+
* this.redirect('http://google.com'); // will format to 'http://google.com/'
|
|
936
|
+
*/
|
|
937
|
+
redirect(url, alt) {
|
|
938
|
+
if (url === "back") url = this._getBackReferrer() || alt || "/";
|
|
939
|
+
if (url.startsWith("https://") || url.startsWith("http://")) url = new URL(url).toString();
|
|
940
|
+
this.set("Location", encodeUrl(url));
|
|
941
|
+
if (!statuses.redirect[this.status]) this.status = 302;
|
|
942
|
+
if (this.ctx.accepts("html")) {
|
|
943
|
+
url = escape(url);
|
|
944
|
+
this.type = "text/html; charset=utf-8";
|
|
945
|
+
this.body = `Redirecting to ${url}.`;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this.type = "text/plain; charset=utf-8";
|
|
949
|
+
this.body = `Redirecting to ${url}.`;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Set Content-Disposition header to "attachment" with optional `filename`.
|
|
953
|
+
*/
|
|
954
|
+
attachment(filename, options) {
|
|
955
|
+
if (filename) this.type = extname(filename);
|
|
956
|
+
this.set("Content-Disposition", contentDisposition(filename, options));
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Set Content-Type response header with `type` through `mime.lookup()`
|
|
960
|
+
* when it does not contain a charset.
|
|
961
|
+
*
|
|
962
|
+
* Examples:
|
|
963
|
+
*
|
|
964
|
+
* this.type = '.html';
|
|
965
|
+
* this.type = 'html';
|
|
966
|
+
* this.type = 'json';
|
|
967
|
+
* this.type = 'application/json';
|
|
968
|
+
* this.type = 'png';
|
|
969
|
+
*/
|
|
970
|
+
set type(type) {
|
|
971
|
+
if (!type) {
|
|
972
|
+
this.remove("Content-Type");
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const mimeType = getType(type);
|
|
976
|
+
if (mimeType) this.set("Content-Type", mimeType);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Return the response mime type void of
|
|
980
|
+
* parameters such as "charset".
|
|
981
|
+
*/
|
|
982
|
+
get type() {
|
|
983
|
+
const type = this.get("Content-Type");
|
|
984
|
+
if (!type) return "";
|
|
985
|
+
return type.split(";", 1)[0];
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Check whether the response is one of the listed types.
|
|
989
|
+
* Pretty much the same as `this.request.is()`.
|
|
990
|
+
*
|
|
991
|
+
* this.response.is('html')
|
|
992
|
+
* this.response.is('html', 'json')
|
|
993
|
+
*/
|
|
994
|
+
is(type, ...types) {
|
|
995
|
+
let testTypes = [];
|
|
996
|
+
if (type) testTypes = Array.isArray(type) ? type : [type];
|
|
997
|
+
return is(this.type, [...testTypes, ...types]);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Set the Last-Modified date using a string or a Date.
|
|
1001
|
+
*
|
|
1002
|
+
* this.response.lastModified = new Date();
|
|
1003
|
+
* this.response.lastModified = '2013-09-13';
|
|
1004
|
+
*/
|
|
1005
|
+
set lastModified(val) {
|
|
1006
|
+
if (typeof val === "string") val = new Date(val);
|
|
1007
|
+
if (val) this.set("Last-Modified", val.toUTCString());
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Get the Last-Modified date in Date form, if it exists.
|
|
1011
|
+
*/
|
|
1012
|
+
get lastModified() {
|
|
1013
|
+
const date = this.get("last-modified");
|
|
1014
|
+
if (date) return new Date(date);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Set the ETag of a response.
|
|
1018
|
+
* This will normalize the quotes if necessary.
|
|
1019
|
+
*
|
|
1020
|
+
* this.response.etag = 'md5-hash-sum';
|
|
1021
|
+
* this.response.etag = '"md5-hash-sum"';
|
|
1022
|
+
* this.response.etag = 'W/"123456789"';
|
|
1023
|
+
*/
|
|
1024
|
+
set etag(val) {
|
|
1025
|
+
if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
|
|
1026
|
+
this.set("ETag", val);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Get the ETag of a response.
|
|
1030
|
+
*/
|
|
1031
|
+
get etag() {
|
|
1032
|
+
return this.get("ETag");
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Return response header.
|
|
1036
|
+
*
|
|
1037
|
+
* Examples:
|
|
1038
|
+
*
|
|
1039
|
+
* this.get('Content-Type');
|
|
1040
|
+
* // => "text/plain"
|
|
1041
|
+
*
|
|
1042
|
+
* this.get('content-type');
|
|
1043
|
+
* // => "text/plain"
|
|
1044
|
+
*/
|
|
1045
|
+
get(field) {
|
|
1046
|
+
return this.header[field.toLowerCase()] || "";
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Returns true if the header identified by name is currently set in the outgoing headers.
|
|
1050
|
+
* The header name matching is case-insensitive.
|
|
1051
|
+
*
|
|
1052
|
+
* Examples:
|
|
1053
|
+
*
|
|
1054
|
+
* this.has('Content-Type');
|
|
1055
|
+
* // => true
|
|
1056
|
+
*
|
|
1057
|
+
* this.get('content-type');
|
|
1058
|
+
* // => true
|
|
1059
|
+
*/
|
|
1060
|
+
has(field) {
|
|
1061
|
+
return this.res.hasHeader(field);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Set header `field` to `val` or pass
|
|
1065
|
+
* an object of header fields.
|
|
1066
|
+
*
|
|
1067
|
+
* Examples:
|
|
1068
|
+
*
|
|
1069
|
+
* this.set('Foo', ['bar', 'baz']);
|
|
1070
|
+
* this.set('Accept', 'application/json');
|
|
1071
|
+
* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
|
|
1072
|
+
*/
|
|
1073
|
+
set(field, val) {
|
|
1074
|
+
if (this.headerSent) return;
|
|
1075
|
+
if (typeof field === "string") {
|
|
1076
|
+
let value = val;
|
|
1077
|
+
if (Array.isArray(val)) value = val.map((v) => typeof v === "string" ? v : String(v));
|
|
1078
|
+
else if (typeof val !== "string") value = String(val);
|
|
1079
|
+
this.res.setHeader(field, value);
|
|
1080
|
+
} else for (const key in field) this.set(key, field[key]);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Append additional header `field` with value `val`.
|
|
1084
|
+
*
|
|
1085
|
+
* Examples:
|
|
1086
|
+
*
|
|
1087
|
+
* ```
|
|
1088
|
+
* this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
|
|
1089
|
+
* this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
|
|
1090
|
+
* this.append('Warning', '199 Miscellaneous warning');
|
|
1091
|
+
*/
|
|
1092
|
+
append(field, val) {
|
|
1093
|
+
const prev = this.get(field);
|
|
1094
|
+
let value = val;
|
|
1095
|
+
if (prev) value = Array.isArray(prev) ? prev.concat(value) : [prev].concat(val);
|
|
1096
|
+
return this.set(field, value);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Remove header `field`.
|
|
1100
|
+
*/
|
|
1101
|
+
remove(field) {
|
|
1102
|
+
if (this.headerSent) return;
|
|
1103
|
+
this.res.removeHeader(field);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Checks if the request is writable.
|
|
1107
|
+
* Tests for the existence of the socket
|
|
1108
|
+
* as node sometimes does not set it.
|
|
1109
|
+
*/
|
|
1110
|
+
get writable() {
|
|
1111
|
+
if (this.res.writableEnded || this.res.finished) return false;
|
|
1112
|
+
const socket = this.res.socket;
|
|
1113
|
+
if (!socket) return true;
|
|
1114
|
+
return socket.writable;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Inspect implementation.
|
|
1118
|
+
*/
|
|
1119
|
+
inspect() {
|
|
1120
|
+
if (!this.res) return;
|
|
1121
|
+
const o = this.toJSON();
|
|
1122
|
+
Reflect.set(o, "body", this.body);
|
|
1123
|
+
return o;
|
|
1124
|
+
}
|
|
1125
|
+
[util.inspect.custom]() {
|
|
1126
|
+
return this.inspect();
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Return JSON representation.
|
|
1130
|
+
*/
|
|
1131
|
+
toJSON() {
|
|
1132
|
+
return {
|
|
1133
|
+
status: this.status,
|
|
1134
|
+
message: this.message,
|
|
1135
|
+
header: this.header
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Flush any set headers and begin the body
|
|
1140
|
+
*/
|
|
1141
|
+
flushHeaders() {
|
|
1142
|
+
this.res.flushHeaders();
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
//#endregion
|
|
1147
|
+
//#region src/application.ts
|
|
1148
|
+
const debug = debuglog("egg/koa/application");
|
|
1149
|
+
/**
|
|
1150
|
+
* Expose `Application` class.
|
|
1151
|
+
* Inherits from `Emitter.prototype`.
|
|
1152
|
+
*/
|
|
1153
|
+
var Application = class extends Emitter {
|
|
1154
|
+
/**
|
|
1155
|
+
* Make HttpError available to consumers of the library so that consumers don't
|
|
1156
|
+
* have a direct dependency upon `http-errors`
|
|
1157
|
+
*/
|
|
1158
|
+
static HttpError = HttpError;
|
|
1159
|
+
_proxy;
|
|
1160
|
+
_env;
|
|
1161
|
+
subdomainOffset;
|
|
1162
|
+
proxyIpHeader;
|
|
1163
|
+
maxIpsCount;
|
|
1164
|
+
_keys;
|
|
1165
|
+
middleware;
|
|
1166
|
+
ctxStorage;
|
|
1167
|
+
silent;
|
|
1168
|
+
ContextClass;
|
|
1169
|
+
context;
|
|
1170
|
+
RequestClass;
|
|
1171
|
+
request;
|
|
1172
|
+
ResponseClass;
|
|
1173
|
+
response;
|
|
1174
|
+
/**
|
|
1175
|
+
* Initialize a new `Application`.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {object} [options] Application options
|
|
1178
|
+
* @param {string} [options.env] Environment, default is `development`
|
|
1179
|
+
* @param {string[]} [options.keys] Signed cookie keys
|
|
1180
|
+
* @param {boolean} [options.proxy] Trust proxy headers
|
|
1181
|
+
* @param {number} [options.subdomainOffset] Subdomain offset
|
|
1182
|
+
* @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
|
|
1183
|
+
* @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
|
|
1184
|
+
*/
|
|
1185
|
+
constructor(options) {
|
|
1186
|
+
super();
|
|
1187
|
+
options = options || {};
|
|
1188
|
+
this._proxy = options.proxy || false;
|
|
1189
|
+
this.subdomainOffset = options.subdomainOffset || 2;
|
|
1190
|
+
this.proxyIpHeader = options.proxyIpHeader || "X-Forwarded-For";
|
|
1191
|
+
this.maxIpsCount = options.maxIpsCount || 0;
|
|
1192
|
+
this._env = options.env || process.env.NODE_ENV || "development";
|
|
1193
|
+
if (options.keys) this._keys = options.keys;
|
|
1194
|
+
this.middleware = [];
|
|
1195
|
+
this.ctxStorage = getAsyncLocalStorage();
|
|
1196
|
+
this.silent = false;
|
|
1197
|
+
this.ContextClass = class ApplicationContext extends Context {};
|
|
1198
|
+
this.context = this.ContextClass.prototype;
|
|
1199
|
+
this.RequestClass = class ApplicationRequest extends Request {};
|
|
1200
|
+
this.request = this.RequestClass.prototype;
|
|
1201
|
+
this.ResponseClass = class ApplicationResponse extends Response {};
|
|
1202
|
+
this.response = this.ResponseClass.prototype;
|
|
1203
|
+
}
|
|
1204
|
+
get keys() {
|
|
1205
|
+
return this._keys;
|
|
1206
|
+
}
|
|
1207
|
+
set keys(value) {
|
|
1208
|
+
this._keys = value;
|
|
1209
|
+
}
|
|
1210
|
+
get env() {
|
|
1211
|
+
return this._env;
|
|
1212
|
+
}
|
|
1213
|
+
set env(value) {
|
|
1214
|
+
this._env = value;
|
|
1215
|
+
}
|
|
1216
|
+
get proxy() {
|
|
1217
|
+
return this._proxy;
|
|
1218
|
+
}
|
|
1219
|
+
set proxy(value) {
|
|
1220
|
+
this._proxy = value;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Shorthand for:
|
|
1224
|
+
*
|
|
1225
|
+
* http.createServer(app.callback()).listen(...)
|
|
1226
|
+
*/
|
|
1227
|
+
listen(...args) {
|
|
1228
|
+
debug("listen with args: %o", args);
|
|
1229
|
+
return http.createServer(this.callback()).listen(...args);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Return JSON representation.
|
|
1233
|
+
* We only bother showing settings.
|
|
1234
|
+
*/
|
|
1235
|
+
toJSON() {
|
|
1236
|
+
return {
|
|
1237
|
+
subdomainOffset: this.subdomainOffset,
|
|
1238
|
+
proxy: this.proxy,
|
|
1239
|
+
env: this.env
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Inspect implementation.
|
|
1244
|
+
*/
|
|
1245
|
+
inspect() {
|
|
1246
|
+
return this.toJSON();
|
|
1247
|
+
}
|
|
1248
|
+
[util.inspect.custom]() {
|
|
1249
|
+
return this.inspect();
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Use the given middleware `fn`.
|
|
1253
|
+
*/
|
|
1254
|
+
use(fn) {
|
|
1255
|
+
if (typeof fn !== "function") throw new TypeError("middleware must be a function!");
|
|
1256
|
+
const name = fn._name || fn.name || "-";
|
|
1257
|
+
if (isGeneratorFunction(fn)) throw new TypeError(`Support for generators was removed, middleware: ${name}. See the documentation for examples of how to convert old middleware https://github.com/koajs/koa/blob/master/docs/migration.md`);
|
|
1258
|
+
debug("use %o #%d", name, this.middleware.length);
|
|
1259
|
+
this.middleware.push(fn);
|
|
1260
|
+
return this;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Return a request handler callback
|
|
1264
|
+
* for node's native http server.
|
|
1265
|
+
*/
|
|
1266
|
+
callback() {
|
|
1267
|
+
const fn = compose(this.middleware);
|
|
1268
|
+
if (!this.listenerCount("error")) this.on("error", this.onerror.bind(this));
|
|
1269
|
+
const handleRequest = (req, res) => {
|
|
1270
|
+
const ctx = this.createContext(req, res);
|
|
1271
|
+
return this.ctxStorage.run(ctx, async () => {
|
|
1272
|
+
return await this.handleRequest(ctx, fn);
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1275
|
+
return handleRequest;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* return current context from async local storage
|
|
1279
|
+
*/
|
|
1280
|
+
get currentContext() {
|
|
1281
|
+
return this.ctxStorage.getStore();
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Handle request in callback.
|
|
1285
|
+
* @private
|
|
1286
|
+
*/
|
|
1287
|
+
async handleRequest(ctx, fnMiddleware) {
|
|
1288
|
+
this.emit("request", ctx);
|
|
1289
|
+
const res = ctx.res;
|
|
1290
|
+
res.statusCode = 404;
|
|
1291
|
+
const onerror = (err) => ctx.onerror(err);
|
|
1292
|
+
onFinished(res, (err) => {
|
|
1293
|
+
if (err) onerror(err);
|
|
1294
|
+
this.emit("response", ctx);
|
|
1295
|
+
});
|
|
1296
|
+
try {
|
|
1297
|
+
await fnMiddleware(ctx);
|
|
1298
|
+
return this._respond(ctx);
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
return onerror(err);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Initialize a new context.
|
|
1305
|
+
* @private
|
|
1306
|
+
*/
|
|
1307
|
+
createContext(req, res) {
|
|
1308
|
+
return new this.ContextClass(this, req, res);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Default error handler.
|
|
1312
|
+
* @private
|
|
1313
|
+
*/
|
|
1314
|
+
onerror(err) {
|
|
1315
|
+
if (!(err instanceof Error || Object.prototype.toString.call(err) === "[object Error]")) throw new TypeError(util.format("non-error thrown: %j", err));
|
|
1316
|
+
if (err.status === 404 || err.expose) return;
|
|
1317
|
+
if (this.silent) return;
|
|
1318
|
+
const msg = err.stack || err.toString();
|
|
1319
|
+
console.error(`\n${msg.replaceAll(/^/gm, " ")}\n`);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Response helper.
|
|
1323
|
+
*/
|
|
1324
|
+
_respond(ctx) {
|
|
1325
|
+
if (ctx.respond === false) return;
|
|
1326
|
+
if (!ctx.writable) return;
|
|
1327
|
+
const res = ctx.res;
|
|
1328
|
+
let body = ctx.body;
|
|
1329
|
+
const code = ctx.status;
|
|
1330
|
+
if (statuses.empty[code]) {
|
|
1331
|
+
ctx.body = null;
|
|
1332
|
+
return res.end();
|
|
1333
|
+
}
|
|
1334
|
+
if (ctx.method === "HEAD") {
|
|
1335
|
+
if (!res.headersSent && !ctx.response.has("Content-Length")) {
|
|
1336
|
+
const { length } = ctx.response;
|
|
1337
|
+
if (Number.isInteger(length)) ctx.length = length;
|
|
1338
|
+
}
|
|
1339
|
+
return res.end();
|
|
1340
|
+
}
|
|
1341
|
+
if (body === null || body === void 0) {
|
|
1342
|
+
if (ctx.response._explicitNullBody) {
|
|
1343
|
+
ctx.response.remove("Content-Type");
|
|
1344
|
+
ctx.response.remove("Transfer-Encoding");
|
|
1345
|
+
return res.end();
|
|
1346
|
+
}
|
|
1347
|
+
if (ctx.req.httpVersionMajor >= 2) body = String(code);
|
|
1348
|
+
else body = ctx.message || String(code);
|
|
1349
|
+
if (!res.headersSent) {
|
|
1350
|
+
ctx.type = "text";
|
|
1351
|
+
ctx.length = Buffer.byteLength(body);
|
|
1352
|
+
}
|
|
1353
|
+
return res.end(body);
|
|
1354
|
+
}
|
|
1355
|
+
if (Buffer.isBuffer(body)) return res.end(body);
|
|
1356
|
+
if (typeof body === "string") return res.end(body);
|
|
1357
|
+
if (body instanceof Stream) return body.pipe(res);
|
|
1358
|
+
body = JSON.stringify(body);
|
|
1359
|
+
if (!res.headersSent) ctx.length = Buffer.byteLength(body);
|
|
1360
|
+
res.end(body);
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
//#endregion
|
|
6
1365
|
//#region src/index.ts
|
|
7
1366
|
var src_default = Application;
|
|
8
1367
|
|