@e22m4u/js-trie-router 0.0.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.
- package/.c8rc +9 -0
- package/.commitlintrc +5 -0
- package/.editorconfig +13 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +5 -0
- package/.mocharc.cjs +4 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/eslint.config.js +34 -0
- package/examples/cookie-parsing-example.js +28 -0
- package/examples/params-parsing-example.js +28 -0
- package/examples/query-parsing-example.js +28 -0
- package/examples/uptime-example.js +40 -0
- package/package.json +57 -0
- package/src/chai.js +7 -0
- package/src/hooks/hook-invoker.d.ts +25 -0
- package/src/hooks/hook-invoker.js +104 -0
- package/src/hooks/hook-invoker.spec.js +462 -0
- package/src/hooks/hook-registry.d.ts +43 -0
- package/src/hooks/hook-registry.js +88 -0
- package/src/hooks/hook-registry.spec.js +165 -0
- package/src/hooks/index.d.ts +2 -0
- package/src/hooks/index.js +2 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +9 -0
- package/src/parsers/body-parser.d.ts +52 -0
- package/src/parsers/body-parser.js +161 -0
- package/src/parsers/body-parser.spec.js +297 -0
- package/src/parsers/cookie-parser.d.ts +21 -0
- package/src/parsers/cookie-parser.js +32 -0
- package/src/parsers/cookie-parser.spec.js +26 -0
- package/src/parsers/index.d.ts +4 -0
- package/src/parsers/index.js +4 -0
- package/src/parsers/query-parser.d.ts +21 -0
- package/src/parsers/query-parser.js +32 -0
- package/src/parsers/query-parser.spec.js +25 -0
- package/src/parsers/request-parser.d.ts +34 -0
- package/src/parsers/request-parser.js +65 -0
- package/src/parsers/request-parser.spec.js +137 -0
- package/src/request-context.d.ts +54 -0
- package/src/request-context.js +108 -0
- package/src/request-context.spec.js +89 -0
- package/src/route-registry.d.ts +39 -0
- package/src/route-registry.js +95 -0
- package/src/route-registry.spec.js +77 -0
- package/src/route.d.ts +82 -0
- package/src/route.js +184 -0
- package/src/route.spec.js +299 -0
- package/src/router-options.d.ts +18 -0
- package/src/router-options.js +41 -0
- package/src/router-options.spec.js +52 -0
- package/src/senders/data-sender.d.ts +15 -0
- package/src/senders/data-sender.js +72 -0
- package/src/senders/data-sender.spec.js +193 -0
- package/src/senders/error-sender.d.ts +25 -0
- package/src/senders/error-sender.js +85 -0
- package/src/senders/error-sender.spec.js +90 -0
- package/src/senders/index.d.ts +2 -0
- package/src/senders/index.js +2 -0
- package/src/service.d.ts +14 -0
- package/src/service.js +28 -0
- package/src/service.spec.js +11 -0
- package/src/trie-router.d.ts +66 -0
- package/src/trie-router.js +189 -0
- package/src/trie-router.spec.js +471 -0
- package/src/types.d.ts +19 -0
- package/src/utils/create-cookie-string.d.ts +6 -0
- package/src/utils/create-cookie-string.js +24 -0
- package/src/utils/create-cookie-string.spec.js +36 -0
- package/src/utils/create-debugger.d.ts +11 -0
- package/src/utils/create-debugger.js +22 -0
- package/src/utils/create-debugger.spec.js +30 -0
- package/src/utils/create-error.d.ts +14 -0
- package/src/utils/create-error.js +28 -0
- package/src/utils/create-error.spec.js +50 -0
- package/src/utils/create-request-mock.d.ts +28 -0
- package/src/utils/create-request-mock.js +345 -0
- package/src/utils/create-request-mock.spec.js +482 -0
- package/src/utils/create-response-mock.d.ts +16 -0
- package/src/utils/create-response-mock.js +119 -0
- package/src/utils/create-response-mock.spec.js +130 -0
- package/src/utils/fetch-request-body.d.ts +17 -0
- package/src/utils/fetch-request-body.js +133 -0
- package/src/utils/fetch-request-body.spec.js +211 -0
- package/src/utils/get-request-path.d.ts +8 -0
- package/src/utils/get-request-path.js +23 -0
- package/src/utils/get-request-path.spec.js +31 -0
- package/src/utils/index.d.ts +11 -0
- package/src/utils/index.js +11 -0
- package/src/utils/is-promise.d.ts +10 -0
- package/src/utils/is-promise.js +13 -0
- package/src/utils/is-promise.spec.js +20 -0
- package/src/utils/is-readable-stream.d.ts +9 -0
- package/src/utils/is-readable-stream.js +11 -0
- package/src/utils/is-readable-stream.spec.js +23 -0
- package/src/utils/is-response-sent.d.ts +8 -0
- package/src/utils/is-response-sent.js +23 -0
- package/src/utils/is-response-sent.spec.js +35 -0
- package/src/utils/is-writable-stream.d.ts +9 -0
- package/src/utils/is-writable-stream.js +11 -0
- package/src/utils/is-writable-stream.spec.js +23 -0
- package/src/utils/parse-content-type.d.ts +15 -0
- package/src/utils/parse-content-type.js +30 -0
- package/src/utils/parse-content-type.spec.js +62 -0
- package/src/utils/parse-cookie.d.ts +19 -0
- package/src/utils/parse-cookie.js +30 -0
- package/src/utils/parse-cookie.spec.js +37 -0
- package/src/utils/to-camel-case.d.ts +6 -0
- package/src/utils/to-camel-case.js +20 -0
- package/src/utils/to-camel-case.spec.js +32 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {describe} from 'mocha';
|
|
2
|
+
import {expect} from '../chai.js';
|
|
3
|
+
import {PassThrough} from 'stream';
|
|
4
|
+
import {createResponseMock} from './create-response-mock.js';
|
|
5
|
+
|
|
6
|
+
describe('createResponseMock', function () {
|
|
7
|
+
it('returns an instance of PassThrough', function () {
|
|
8
|
+
const res = createResponseMock();
|
|
9
|
+
expect(res).to.be.instanceof(PassThrough);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('setEncoding', function () {
|
|
13
|
+
it('sets the given encoding and returns the response', function () {
|
|
14
|
+
const res = createResponseMock();
|
|
15
|
+
expect(res._encoding).to.be.undefined;
|
|
16
|
+
const ret = res.setEncoding('utf-8');
|
|
17
|
+
expect(ret).to.be.eq(res);
|
|
18
|
+
expect(res._encoding).to.be.eq('utf-8');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getEncoding', function () {
|
|
23
|
+
it('returns encoding', function () {
|
|
24
|
+
const res = createResponseMock();
|
|
25
|
+
expect(res._encoding).to.be.undefined;
|
|
26
|
+
const ret1 = res.getEncoding();
|
|
27
|
+
expect(ret1).to.be.undefined;
|
|
28
|
+
res._encoding = 'utf-8';
|
|
29
|
+
const ret2 = res.getEncoding();
|
|
30
|
+
expect(ret2).to.be.eq('utf-8');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('headersSent', function () {
|
|
35
|
+
it('returns false if the response is not sent', function () {
|
|
36
|
+
const res = createResponseMock();
|
|
37
|
+
expect(res._headersSent).to.be.false;
|
|
38
|
+
expect(res.headersSent).to.be.false;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns a value of the "_headersSent" property', function () {
|
|
42
|
+
const res = createResponseMock();
|
|
43
|
+
expect(res._headersSent).to.be.false;
|
|
44
|
+
expect(res.headersSent).to.be.false;
|
|
45
|
+
res._headersSent = true;
|
|
46
|
+
expect(res.headersSent).to.be.true;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('setHeader', function () {
|
|
51
|
+
it('sets the given header and returns the response', function () {
|
|
52
|
+
const res = createResponseMock();
|
|
53
|
+
expect(res._headers['foo']).to.be.eq(undefined);
|
|
54
|
+
const ret = res.setHeader('foo', 'bar');
|
|
55
|
+
expect(ret).to.be.eq(res);
|
|
56
|
+
expect(res._headers['foo']).to.be.eq('bar');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws an error if headers is sent', function () {
|
|
60
|
+
const res = createResponseMock();
|
|
61
|
+
res._headersSent = true;
|
|
62
|
+
const throwable = () => res.setHeader('foo');
|
|
63
|
+
expect(throwable).to.throw(
|
|
64
|
+
'Error [ERR_HTTP_HEADERS_SENT]: ' +
|
|
65
|
+
'Cannot set headers after they are sent to the client',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('sets the header value as a string', function () {
|
|
70
|
+
const res = createResponseMock();
|
|
71
|
+
expect(res._headers['num']).to.be.eq(undefined);
|
|
72
|
+
const ret = res.setHeader('num', 10);
|
|
73
|
+
expect(ret).to.be.eq(res);
|
|
74
|
+
expect(res._headers['num']).to.be.eq('10');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('getHeader', function () {
|
|
79
|
+
it('returns the header value if exists', function () {
|
|
80
|
+
const res = createResponseMock();
|
|
81
|
+
res._headers['foo'] = 'bar';
|
|
82
|
+
const ret = res.getHeader('foo');
|
|
83
|
+
expect(ret).to.be.eq('bar');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('uses case-insensitivity lookup', function () {
|
|
87
|
+
const res = createResponseMock();
|
|
88
|
+
res._headers['foo'] = 'bar';
|
|
89
|
+
const ret = res.getHeader('FOO');
|
|
90
|
+
expect(ret).to.be.eq('bar');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getHeaders', function () {
|
|
95
|
+
it('returns a copy of the headers object', function () {
|
|
96
|
+
const res = createResponseMock();
|
|
97
|
+
const ret1 = res.getHeaders();
|
|
98
|
+
res._headers['foo'] = 'bar';
|
|
99
|
+
res._headers['baz'] = 'qux';
|
|
100
|
+
const ret2 = res.getHeaders();
|
|
101
|
+
expect(ret1).to.be.eql({});
|
|
102
|
+
expect(ret2).to.be.eql({foo: 'bar', baz: 'qux'});
|
|
103
|
+
expect(ret1).not.to.be.eq(res._headers);
|
|
104
|
+
expect(ret2).not.to.be.eq(res._headers);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('getBody', function () {
|
|
109
|
+
it('returns a promise of the stream content', async function () {
|
|
110
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
111
|
+
const res = createResponseMock();
|
|
112
|
+
res.end(body);
|
|
113
|
+
const promise = res.getBody();
|
|
114
|
+
expect(promise).to.be.instanceof(Promise);
|
|
115
|
+
await expect(promise).to.eventually.be.eq(body);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Stream', function () {
|
|
120
|
+
it('sets "headerSent" to true when the stream ends', function (done) {
|
|
121
|
+
const res = createResponseMock();
|
|
122
|
+
expect(res.headersSent).to.be.false;
|
|
123
|
+
res.on('end', () => {
|
|
124
|
+
expect(res.headersSent).to.be.true;
|
|
125
|
+
done();
|
|
126
|
+
});
|
|
127
|
+
res.end('test');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {IncomingMessage} from 'http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Buffer encoding list.
|
|
5
|
+
*/
|
|
6
|
+
export type BUFFER_ENCODING_LIST = BufferEncoding[];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fetch request body.
|
|
10
|
+
*
|
|
11
|
+
* @param req
|
|
12
|
+
* @param bodyBytesLimit
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchRequestBody(
|
|
15
|
+
req: IncomingMessage,
|
|
16
|
+
bodyBytesLimit?: number,
|
|
17
|
+
): Promise<string>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import HttpErrors from 'http-errors';
|
|
2
|
+
import {createError} from './create-error.js';
|
|
3
|
+
import {parseContentType} from './parse-content-type.js';
|
|
4
|
+
import {Errorf} from '@e22m4u/js-format';
|
|
5
|
+
import {IncomingMessage} from 'http';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Buffer encoding.
|
|
9
|
+
*
|
|
10
|
+
* @type {import('buffer').BufferEncoding[]}
|
|
11
|
+
*/
|
|
12
|
+
export const BUFFER_ENCODING_LIST = [
|
|
13
|
+
'ascii',
|
|
14
|
+
'utf8',
|
|
15
|
+
'utf-8',
|
|
16
|
+
'utf16le',
|
|
17
|
+
'utf-16le',
|
|
18
|
+
'ucs2',
|
|
19
|
+
'ucs-2',
|
|
20
|
+
'base64',
|
|
21
|
+
'base64url',
|
|
22
|
+
'latin1',
|
|
23
|
+
'binary',
|
|
24
|
+
'hex',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetch request body.
|
|
29
|
+
*
|
|
30
|
+
* @param {IncomingMessage} req
|
|
31
|
+
* @param {number} bodyBytesLimit
|
|
32
|
+
* @returns {Promise<string|undefined>}
|
|
33
|
+
*/
|
|
34
|
+
export function fetchRequestBody(req, bodyBytesLimit = 0) {
|
|
35
|
+
if (!(req instanceof IncomingMessage))
|
|
36
|
+
throw new Errorf(
|
|
37
|
+
'The first parameter of "fetchRequestBody" should be ' +
|
|
38
|
+
'an IncomingMessage instance, but %v given.',
|
|
39
|
+
req,
|
|
40
|
+
);
|
|
41
|
+
if (typeof bodyBytesLimit !== 'number')
|
|
42
|
+
throw new Errorf(
|
|
43
|
+
'The parameter "bodyBytesLimit" of "fetchRequestBody" ' +
|
|
44
|
+
'should be a number, but %v given.',
|
|
45
|
+
bodyBytesLimit,
|
|
46
|
+
);
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
// сравнение внутреннего ограничения
|
|
49
|
+
// размера тела запроса с заголовком
|
|
50
|
+
// "content-length"
|
|
51
|
+
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
52
|
+
if (bodyBytesLimit && contentLength && contentLength > bodyBytesLimit)
|
|
53
|
+
throw createError(
|
|
54
|
+
HttpErrors.PayloadTooLarge,
|
|
55
|
+
'Request body limit is %s bytes, but %s bytes given.',
|
|
56
|
+
bodyBytesLimit,
|
|
57
|
+
contentLength,
|
|
58
|
+
);
|
|
59
|
+
// определение кодировки
|
|
60
|
+
// по заголовку "content-type"
|
|
61
|
+
let encoding = 'utf-8';
|
|
62
|
+
const contentType = req.headers['content-type'] || '';
|
|
63
|
+
if (contentType) {
|
|
64
|
+
const parsedContentType = parseContentType(contentType);
|
|
65
|
+
if (parsedContentType && parsedContentType.charset) {
|
|
66
|
+
encoding = parsedContentType.charset.toLowerCase();
|
|
67
|
+
if (!BUFFER_ENCODING_LIST.includes(encoding))
|
|
68
|
+
throw createError(
|
|
69
|
+
HttpErrors.UnsupportedMediaType,
|
|
70
|
+
'Request encoding %v is not supported.',
|
|
71
|
+
encoding,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// подготовка массива загружаемых байтов
|
|
76
|
+
// и счетчика для отслеживания их объема
|
|
77
|
+
const data = [];
|
|
78
|
+
let receivedLength = 0;
|
|
79
|
+
// обработчик проверяет объем загружаемых
|
|
80
|
+
// данных и складывает их в массив
|
|
81
|
+
const onData = chunk => {
|
|
82
|
+
receivedLength += chunk.length;
|
|
83
|
+
if (bodyBytesLimit && receivedLength > bodyBytesLimit) {
|
|
84
|
+
req.removeAllListeners();
|
|
85
|
+
const error = createError(
|
|
86
|
+
HttpErrors.PayloadTooLarge,
|
|
87
|
+
'Request body limit is %v bytes, but %v bytes given.',
|
|
88
|
+
bodyBytesLimit,
|
|
89
|
+
receivedLength,
|
|
90
|
+
);
|
|
91
|
+
reject(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
data.push(chunk);
|
|
95
|
+
};
|
|
96
|
+
// кода данные полностью загружены, нужно удалить
|
|
97
|
+
// обработчики событий, и сравнить полученный объем
|
|
98
|
+
// данных с заявленным в заголовке "content-length"
|
|
99
|
+
const onEnd = () => {
|
|
100
|
+
req.removeAllListeners();
|
|
101
|
+
if (contentLength && contentLength !== receivedLength) {
|
|
102
|
+
const error = createError(
|
|
103
|
+
HttpErrors.BadRequest,
|
|
104
|
+
'Received bytes do not match the "content-length" header.',
|
|
105
|
+
);
|
|
106
|
+
reject(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// объединение массива байтов в буфер,
|
|
110
|
+
// кодирование результата в строку,
|
|
111
|
+
// и передача полученных данных
|
|
112
|
+
// в ожидающий Promise
|
|
113
|
+
const buffer = Buffer.concat(data);
|
|
114
|
+
const body = Buffer.from(buffer, encoding).toString();
|
|
115
|
+
resolve(body || undefined);
|
|
116
|
+
};
|
|
117
|
+
// при ошибке загрузки тела запроса,
|
|
118
|
+
// удаляются обработчики событий,
|
|
119
|
+
// и отклоняется ожидающий Promise
|
|
120
|
+
// ошибкой с кодом 400
|
|
121
|
+
const onError = error => {
|
|
122
|
+
req.removeAllListeners();
|
|
123
|
+
reject(HttpErrors(400, error));
|
|
124
|
+
};
|
|
125
|
+
// добавление обработчиков прослушивающих
|
|
126
|
+
// события входящего запроса и возобновление
|
|
127
|
+
// потока данных
|
|
128
|
+
req.on('data', onData);
|
|
129
|
+
req.on('end', onEnd);
|
|
130
|
+
req.on('error', onError);
|
|
131
|
+
req.resume();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {expect} from '../chai.js';
|
|
2
|
+
import {format} from '@e22m4u/js-format';
|
|
3
|
+
import {fetchRequestBody} from './fetch-request-body.js';
|
|
4
|
+
import {createRequestMock} from './create-request-mock.js';
|
|
5
|
+
|
|
6
|
+
describe('fetchRequestBody', function () {
|
|
7
|
+
it('requires the first parameter to be an IncomingMessage instance', function () {
|
|
8
|
+
const throwable = v => () => fetchRequestBody(v);
|
|
9
|
+
const error = v =>
|
|
10
|
+
format(
|
|
11
|
+
'The first parameter of "fetchRequestBody" should be ' +
|
|
12
|
+
'an IncomingMessage instance, but %s given.',
|
|
13
|
+
v,
|
|
14
|
+
);
|
|
15
|
+
expect(throwable('str')).to.throw(error('"str"'));
|
|
16
|
+
expect(throwable('')).to.throw(error('""'));
|
|
17
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
18
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
19
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
20
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
21
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
22
|
+
expect(throwable({})).to.throw(error('Object'));
|
|
23
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
24
|
+
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
25
|
+
throwable(createRequestMock())();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('requires the parameter "bodyBytesLimit" to be an IncomingMessage instance', function () {
|
|
29
|
+
const req = createRequestMock();
|
|
30
|
+
const throwable = v => () => fetchRequestBody(req, v);
|
|
31
|
+
const error = v =>
|
|
32
|
+
format(
|
|
33
|
+
'The parameter "bodyBytesLimit" of "fetchRequestBody" ' +
|
|
34
|
+
'should be a number, but %s given.',
|
|
35
|
+
v,
|
|
36
|
+
);
|
|
37
|
+
expect(throwable('str')).to.throw(error('"str"'));
|
|
38
|
+
expect(throwable('')).to.throw(error('""'));
|
|
39
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
40
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
41
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
42
|
+
expect(throwable({})).to.throw(error('Object'));
|
|
43
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
44
|
+
throwable(10)();
|
|
45
|
+
throwable(0)();
|
|
46
|
+
throwable(undefined)();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns a string from the string body', async function () {
|
|
50
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
51
|
+
const req = createRequestMock({body});
|
|
52
|
+
const result = await fetchRequestBody(req);
|
|
53
|
+
expect(result).to.be.eq(body);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns a string from the buffer body', async function () {
|
|
57
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
58
|
+
const req = createRequestMock({body: Buffer.from(body)});
|
|
59
|
+
const result = await fetchRequestBody(req);
|
|
60
|
+
expect(result).to.be.eq(body);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('encoding of the header "content-type"', function () {
|
|
64
|
+
it('throws an error for an unsupported encoding', async function () {
|
|
65
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
66
|
+
const req = createRequestMock({
|
|
67
|
+
body,
|
|
68
|
+
headers: {'content-type': 'text/plain; charset=unknown'},
|
|
69
|
+
});
|
|
70
|
+
const promise = fetchRequestBody(req);
|
|
71
|
+
await expect(promise).to.be.rejectedWith(
|
|
72
|
+
'Request encoding "unknown" is not supported.',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not throw an error if the header "content-type" does not have a specified charset', async function () {
|
|
77
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
78
|
+
const req = createRequestMock({
|
|
79
|
+
body,
|
|
80
|
+
headers: {'content-type': 'text/plain'},
|
|
81
|
+
});
|
|
82
|
+
const result = await fetchRequestBody(req);
|
|
83
|
+
expect(result).to.be.eq(body);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('decodes non-standard encoding', async function () {
|
|
87
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
88
|
+
const encoding = 'base64';
|
|
89
|
+
const encodedBody = Buffer.from(body).toString(encoding);
|
|
90
|
+
const req = createRequestMock({
|
|
91
|
+
body: Buffer.from(encodedBody, encoding),
|
|
92
|
+
headers: {'content-type': `text/plain; charset=${encoding}`},
|
|
93
|
+
});
|
|
94
|
+
const result = await fetchRequestBody(req);
|
|
95
|
+
expect(result).to.be.eq(body);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('the header "content-length"', function () {
|
|
100
|
+
it('throws an error if the body length is greater than the header', async function () {
|
|
101
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
102
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
103
|
+
const contentLength = String(bodyLength + 10);
|
|
104
|
+
const req = createRequestMock({
|
|
105
|
+
body,
|
|
106
|
+
headers: {'content-length': contentLength},
|
|
107
|
+
});
|
|
108
|
+
const promise = fetchRequestBody(req);
|
|
109
|
+
await expect(promise).to.be.rejectedWith(
|
|
110
|
+
'Received bytes do not match the "content-length" header.',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('throws an error if the body length is lower than the header', async function () {
|
|
115
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
116
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
117
|
+
const contentLength = String(bodyLength - 10);
|
|
118
|
+
const req = createRequestMock({
|
|
119
|
+
body,
|
|
120
|
+
headers: {'content-length': contentLength},
|
|
121
|
+
});
|
|
122
|
+
const promise = fetchRequestBody(req);
|
|
123
|
+
await expect(promise).to.be.rejectedWith(
|
|
124
|
+
'Received bytes do not match the "content-length" header.',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('does not throw an error if the body length does match with the header', async function () {
|
|
129
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
130
|
+
const contentLength = String(Buffer.from(body).byteLength);
|
|
131
|
+
const req = createRequestMock({
|
|
132
|
+
body,
|
|
133
|
+
headers: {'content-length': contentLength},
|
|
134
|
+
});
|
|
135
|
+
const result = await fetchRequestBody(req);
|
|
136
|
+
expect(result).to.be.eq(body);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('the parameter "bodyBytesLimit"', function () {
|
|
141
|
+
it('throws an error if the "content-length" header is greater than the limit', async function () {
|
|
142
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
143
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
144
|
+
const bodyLimit = bodyLength - 10;
|
|
145
|
+
const req = createRequestMock({
|
|
146
|
+
body,
|
|
147
|
+
headers: {'content-length': String(bodyLength)},
|
|
148
|
+
});
|
|
149
|
+
const error = format(
|
|
150
|
+
'Request body limit is %s bytes, but %s bytes given.',
|
|
151
|
+
bodyLimit,
|
|
152
|
+
bodyLength,
|
|
153
|
+
);
|
|
154
|
+
const promise = fetchRequestBody(req, bodyLimit);
|
|
155
|
+
await expect(promise).to.be.rejectedWith(error);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('does not throw an error if the "content-length" header does match with the limit', async function () {
|
|
159
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
160
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
161
|
+
const req = createRequestMock({
|
|
162
|
+
body,
|
|
163
|
+
headers: {'content-length': String(bodyLength)},
|
|
164
|
+
});
|
|
165
|
+
const result = await fetchRequestBody(req, bodyLength);
|
|
166
|
+
expect(result).to.be.eq(body);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not throw an error if the "content-length" header is lower than the limit', async function () {
|
|
170
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
171
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
172
|
+
const req = createRequestMock({
|
|
173
|
+
body,
|
|
174
|
+
headers: {'content-length': String(bodyLength)},
|
|
175
|
+
});
|
|
176
|
+
const result = await fetchRequestBody(req, bodyLength + 10);
|
|
177
|
+
expect(result).to.be.eq(body);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('throws an error if the body length is greater than the limit', async function () {
|
|
181
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
182
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
183
|
+
const bodyLimit = bodyLength - 10;
|
|
184
|
+
const req = createRequestMock({body});
|
|
185
|
+
const error = format(
|
|
186
|
+
'Request body limit is %s bytes, but %s bytes given.',
|
|
187
|
+
bodyLimit,
|
|
188
|
+
bodyLength,
|
|
189
|
+
);
|
|
190
|
+
const promise = fetchRequestBody(req, bodyLimit);
|
|
191
|
+
await expect(promise).to.be.rejectedWith(error);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not throw an error if the body length does match with the limit', async function () {
|
|
195
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
196
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
197
|
+
const req = createRequestMock({body});
|
|
198
|
+
const result = await fetchRequestBody(req, bodyLength);
|
|
199
|
+
expect(result).to.be.eq(body);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not throw an error if the body length is lower than the limit', async function () {
|
|
203
|
+
const body = 'Lorem Ipsum is simply dummy text.';
|
|
204
|
+
const bodyLength = Buffer.from(body).byteLength;
|
|
205
|
+
const bodyLimit = bodyLength + 10;
|
|
206
|
+
const req = createRequestMock({body});
|
|
207
|
+
const result = await fetchRequestBody(req, bodyLimit);
|
|
208
|
+
expect(result).to.be.eq(body);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Errorf} from '@e22m4u/js-format';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get request path.
|
|
5
|
+
*
|
|
6
|
+
* @param {import('http').IncomingMessage} req
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function getRequestPath(req) {
|
|
10
|
+
if (
|
|
11
|
+
!req ||
|
|
12
|
+
typeof req !== 'object' ||
|
|
13
|
+
Array.isArray(req) ||
|
|
14
|
+
typeof req.url !== 'string'
|
|
15
|
+
) {
|
|
16
|
+
throw new Errorf(
|
|
17
|
+
'The first argument of "getRequestPath" should be ' +
|
|
18
|
+
'an instance of IncomingMessage, but %v given.',
|
|
19
|
+
req,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return (req.url || '/').replace(/\?.*$/, '');
|
|
23
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {expect} from '../chai.js';
|
|
2
|
+
import {format} from '@e22m4u/js-format';
|
|
3
|
+
import {getRequestPath} from './get-request-path.js';
|
|
4
|
+
|
|
5
|
+
describe('getRequestPath', function () {
|
|
6
|
+
it('requires the argument to be an Object with "url" property', function () {
|
|
7
|
+
const throwable = v => () => getRequestPath(v);
|
|
8
|
+
const error = v =>
|
|
9
|
+
format(
|
|
10
|
+
'The first argument of "getRequestPath" should be ' +
|
|
11
|
+
'an instance of IncomingMessage, but %s given.',
|
|
12
|
+
v,
|
|
13
|
+
);
|
|
14
|
+
expect(throwable('str')).to.throw(error('"str"'));
|
|
15
|
+
expect(throwable('')).to.throw(error('""'));
|
|
16
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
17
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
18
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
19
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
20
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
21
|
+
expect(throwable({})).to.throw(error('Object'));
|
|
22
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
23
|
+
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
24
|
+
throwable({url: ''})();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns the request path without query parameters', function () {
|
|
28
|
+
const res = getRequestPath({url: '/test?foo=bar'});
|
|
29
|
+
expect(res).to.be.eq('/test');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './is-promise.js';
|
|
2
|
+
export * from './parse-cookie.js';
|
|
3
|
+
export * from './create-error.js';
|
|
4
|
+
export * from './to-camel-case.js';
|
|
5
|
+
export * from './create-debugger.js';
|
|
6
|
+
export * from './is-response-sent.js';
|
|
7
|
+
export * from './get-request-path.js';
|
|
8
|
+
export * from './is-readable-stream.js';
|
|
9
|
+
export * from './is-writable-stream.js';
|
|
10
|
+
export * from './fetch-request-body.js';
|
|
11
|
+
export * from './create-cookie-string.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './is-promise.js';
|
|
2
|
+
export * from './parse-cookie.js';
|
|
3
|
+
export * from './create-error.js';
|
|
4
|
+
export * from './to-camel-case.js';
|
|
5
|
+
export * from './create-debugger.js';
|
|
6
|
+
export * from './is-response-sent.js';
|
|
7
|
+
export * from './get-request-path.js';
|
|
8
|
+
export * from './is-readable-stream.js';
|
|
9
|
+
export * from './is-writable-stream.js';
|
|
10
|
+
export * from './fetch-request-body.js';
|
|
11
|
+
export * from './create-cookie-string.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether a value is a Promise-like
|
|
3
|
+
* instance. Recognizes both native promises
|
|
4
|
+
* and third-party promise libraries.
|
|
5
|
+
*
|
|
6
|
+
* @param {*} value
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function isPromise(value) {
|
|
10
|
+
if (!value) return false;
|
|
11
|
+
if (typeof value !== 'object') return false;
|
|
12
|
+
return typeof value.then === 'function';
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {expect} from '../chai.js';
|
|
2
|
+
import {isPromise} from './is-promise.js';
|
|
3
|
+
|
|
4
|
+
describe('isPromise', function () {
|
|
5
|
+
it('returns true if the value is a promise', function () {
|
|
6
|
+
const value = Promise.resolve();
|
|
7
|
+
expect(isPromise(value)).to.be.true;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns false if the value is not a promise', function () {
|
|
11
|
+
expect(isPromise('string')).to.be.false;
|
|
12
|
+
expect(isPromise(5)).to.be.false;
|
|
13
|
+
expect(isPromise([])).to.be.false;
|
|
14
|
+
expect(isPromise({})).to.be.false;
|
|
15
|
+
expect(isPromise(undefined)).to.be.false;
|
|
16
|
+
expect(isPromise(null)).to.be.false;
|
|
17
|
+
expect(isPromise(NaN)).to.be.false;
|
|
18
|
+
expect(isPromise(() => 10)).to.be.false;
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {expect} from '../chai.js';
|
|
2
|
+
import {Readable} from 'stream';
|
|
3
|
+
import {isReadableStream} from './is-readable-stream.js';
|
|
4
|
+
|
|
5
|
+
describe('isReadableStream', function () {
|
|
6
|
+
it('returns true if the value is a readable stream', function () {
|
|
7
|
+
const value1 = new Readable();
|
|
8
|
+
expect(isReadableStream(value1)).to.be.true;
|
|
9
|
+
const value2 = {pipe: () => undefined};
|
|
10
|
+
expect(isReadableStream(value2)).to.be.true;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns false if the value is not a stream', function () {
|
|
14
|
+
expect(isReadableStream('string')).to.be.false;
|
|
15
|
+
expect(isReadableStream(5)).to.be.false;
|
|
16
|
+
expect(isReadableStream([])).to.be.false;
|
|
17
|
+
expect(isReadableStream({})).to.be.false;
|
|
18
|
+
expect(isReadableStream(undefined)).to.be.false;
|
|
19
|
+
expect(isReadableStream(null)).to.be.false;
|
|
20
|
+
expect(isReadableStream(NaN)).to.be.false;
|
|
21
|
+
expect(isReadableStream(() => 10)).to.be.false;
|
|
22
|
+
});
|
|
23
|
+
});
|