@e22m4u/js-trie-router 0.6.5 → 0.6.6

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/README.md CHANGED
@@ -190,39 +190,38 @@ router.defineRoute({
190
190
 
191
191
  **При получении входящего HTTP-запроса**
192
192
 
193
- 1. [Глобальные хуки `onRequest`](#onrequest)
194
- \- Вызываются до поиска маршрута и создания контекста. Подходит для ранней
195
- блокировки запроса или установки базовых заголовков.
193
+ 1. [Глобальные хуки `onRequest`](#onrequest).
194
+ \- Вызываются до поиска маршрута и разбора входящих данных.
196
195
 
197
- 2. Поиск маршрута
198
- \- Маршрутизатор ищет совпадение пути. Если маршрут не найден,
199
- отправляется ответ `404`, а дальнейшая обработка прекращается.
196
+ 2. Поиск маршрута.
197
+ \- Если маршрут не найден, отправляется ответ `404`, а дальнейшая обработка
198
+ прерывается.
200
199
 
201
- 3. Создание `RequestContext` и парсинг
200
+ 3. Создание `RequestContext` и парсинг.
202
201
  \- Создается экземпляр контекста, разбирается строка запроса, заголовки
203
202
  и извлекается тело запроса.
204
203
 
205
- 4. [Глобальные хуки `preHandler`](#prehandler-глобальный)
206
- \- Вызываются последовательно. Если хук возвращает значение
207
- или отправляет ответ, цикл прерывается.
204
+ 4. [Глобальные хуки `preHandler`](#prehandler-глобальный).
205
+ \- Вызываются последовательно. Если хук возвращает значение или отправляет
206
+ ответ, цикл прерывается.
208
207
 
209
- 5. [Хуки маршрута `preHandler`](#prehandler-для-маршрута)
210
- \- Вызываются последовательно для конкретного маршрута. Правила
211
- прерывания такие же, как у одноименных глобальных хуков.
208
+ 5. [Хуки маршрута `preHandler`](#prehandler-для-маршрута).
209
+ \- Вызываются для конкретного маршрута. Правила прерывания такие же,
210
+ как у глобальных хуков.
212
211
 
213
- 6. Основной обработчик (функция `handler`)
214
- \- Вызывается для формирования ответа сервера.
212
+ 6. Основной обработчик (функция `handler`).
213
+ \- Обработчик запроса вызывается для формирования ответа сервера.
215
214
 
216
- 7. [Хуки маршрута `postHandler`](#posthandler-для-маршрута)
217
- \- Вызываются последовательно. Получают результат работы обработчика
218
- (или из `preHandler`) и могут трансформировать его перед отправкой.
215
+ 7. [Хуки маршрута `postHandler`](#posthandler-для-маршрута).
216
+ \- Получают результат обработчика и могут трансформировать его перед
217
+ отправкой.
219
218
 
220
- 8. [Глобальные хуки `postHandler`](#posthandler-глобальный)
219
+ 8. [Глобальные хуки `postHandler`](#posthandler-глобальный).
221
220
  \- Завершают цепочку трансформации ответа.
222
221
 
223
222
  9. Отправка ответа.
224
- \- Маршрутизатор автоматически форматирует и отправляет итоговые данные
225
- клиенту (если ответ не был отправлен ранее вручную).
223
+ \- Маршрутизатор сериализует итоговые данные, устанавливает заголовки
224
+ и отправляет клиенту.
226
225
 
227
226
  Процесс обработки запроса обернут в глобальный `try/catch` блок. Любая ошибка,
228
227
  выброшенная на любом этапе (в хуках, при разборе тела или в самом обработчике),
@@ -38,7 +38,6 @@ __export(index_exports, {
38
38
  EXPOSED_ERROR_PROPERTIES: () => EXPOSED_ERROR_PROPERTIES,
39
39
  ErrorSender: () => ErrorSender,
40
40
  HttpMethod: () => HttpMethod,
41
- METHODS_WITH_BODY: () => METHODS_WITH_BODY,
42
41
  QueryParser: () => QueryParser,
43
42
  ROOT_PATH: () => ROOT_PATH,
44
43
  ROUTER_HOOK_TYPES: () => ROUTER_HOOK_TYPES,
@@ -51,7 +50,6 @@ __export(index_exports, {
51
50
  RouterHookType: () => RouterHookType,
52
51
  RouterOptions: () => RouterOptions,
53
52
  TrieRouter: () => TrieRouter,
54
- UNPARSABLE_MEDIA_TYPES: () => UNPARSABLE_MEDIA_TYPES,
55
53
  cloneDeep: () => cloneDeep,
56
54
  createCookieString: () => createCookieString,
57
55
  createError: () => createError,
@@ -60,6 +58,7 @@ __export(index_exports, {
60
58
  createRouteMock: () => createRouteMock,
61
59
  fetchRequestBody: () => fetchRequestBody,
62
60
  getRequestPathname: () => getRequestPathname,
61
+ hasRequestBody: () => hasRequestBody,
63
62
  isPromise: () => isPromise,
64
63
  isReadableStream: () => isReadableStream,
65
64
  isResponseSent: () => isResponseSent,
@@ -219,6 +218,18 @@ function isResponseSent(response) {
219
218
  }
220
219
  __name(isResponseSent, "isResponseSent");
221
220
 
221
+ // src/utils/has-request-body.js
222
+ function hasRequestBody(request) {
223
+ if (request.headers["transfer-encoding"] !== void 0) {
224
+ return true;
225
+ }
226
+ if (!isNaN(request.headers["content-length"]) && request.headers["content-length"] !== "0") {
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ __name(hasRequestBody, "hasRequestBody");
232
+
222
233
  // src/utils/create-route-mock.js
223
234
  function createRouteMock(options = {}) {
224
235
  return new Route({
@@ -641,7 +652,7 @@ function createRequestHeaders(host, secure, body, cookies, encoding, headers) {
641
652
  obj["content-type"] = "application/json";
642
653
  }
643
654
  }
644
- if (body != null && obj["content-length"] == null) {
655
+ if (body != null && obj["transfer-encoding"] == null && obj["content-length"] == null) {
645
656
  if (typeof body === "string") {
646
657
  const length = Buffer.byteLength(body, encoding);
647
658
  obj["content-length"] = String(length);
@@ -1558,8 +1569,6 @@ var RouterOptions = _RouterOptions;
1558
1569
 
1559
1570
  // src/parsers/body-parser.js
1560
1571
  var import_js_format15 = require("@e22m4u/js-format");
1561
- var METHODS_WITH_BODY = ["POST", "PUT", "PATCH", "DELETE"];
1562
- var UNPARSABLE_MEDIA_TYPES = ["multipart/form-data"];
1563
1572
  var _BodyParser = class _BodyParser extends DebuggableService {
1564
1573
  /**
1565
1574
  * Parsers.
@@ -1590,7 +1599,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
1590
1599
  parser
1591
1600
  );
1592
1601
  }
1593
- this._parsers[mediaType] = parser;
1602
+ this._parsers[mediaType.toLowerCase()] = parser;
1594
1603
  return this;
1595
1604
  }
1596
1605
  /**
@@ -1606,7 +1615,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
1606
1615
  mediaType
1607
1616
  );
1608
1617
  }
1609
- return Boolean(this._parsers[mediaType]);
1618
+ return Boolean(this._parsers[mediaType.toLowerCase()]);
1610
1619
  }
1611
1620
  /**
1612
1621
  * Get parser.
@@ -1621,7 +1630,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
1621
1630
  mediaType
1622
1631
  );
1623
1632
  }
1624
- const parser = this._parsers[mediaType];
1633
+ const parser = this._parsers[mediaType.toLowerCase()];
1625
1634
  if (!parser) {
1626
1635
  throw new import_js_format15.InvalidArgumentError(
1627
1636
  "Media type %v does not have a parser.",
@@ -1643,7 +1652,7 @@ var _BodyParser = class _BodyParser extends DebuggableService {
1643
1652
  mediaType
1644
1653
  );
1645
1654
  }
1646
- delete this._parsers[mediaType];
1655
+ delete this._parsers[mediaType.toLowerCase()];
1647
1656
  return this;
1648
1657
  }
1649
1658
  /**
@@ -1659,17 +1668,11 @@ var _BodyParser = class _BodyParser extends DebuggableService {
1659
1668
  request.method.toUpperCase(),
1660
1669
  getRequestPathname(request)
1661
1670
  );
1662
- if (!METHODS_WITH_BODY.includes(request.method.toUpperCase())) {
1663
- debug(
1664
- "Skipping body parsing for the %s method.",
1665
- request.method.toUpperCase()
1666
- );
1671
+ if (!hasRequestBody(request)) {
1672
+ debug("Skipping body parsing because no body is provided.");
1667
1673
  return;
1668
1674
  }
1669
- const contentType = (request.headers["content-type"] || "").replace(
1670
- /^([^;]+);.*$/,
1671
- "$1"
1672
- );
1675
+ const contentType = request.headers["content-type"];
1673
1676
  if (!contentType) {
1674
1677
  debug("Skipping body parsing because no content type is provided.");
1675
1678
  return;
@@ -1681,17 +1684,10 @@ var _BodyParser = class _BodyParser extends DebuggableService {
1681
1684
  'Unable to parse the "content-type" header.'
1682
1685
  );
1683
1686
  }
1684
- const parser = this._parsers[mediaType];
1687
+ const parser = this._parsers[mediaType.toLowerCase()];
1685
1688
  if (!parser) {
1686
- if (UNPARSABLE_MEDIA_TYPES.includes(mediaType)) {
1687
- debug("Skipping body parsing for the media type %v.", mediaType);
1688
- return;
1689
- }
1690
- throw createError(
1691
- import_http_errors2.default.UnsupportedMediaType,
1692
- "Media type %v is not supported.",
1693
- mediaType
1694
- );
1689
+ debug("No body parser for the media type %v.", mediaType);
1690
+ return;
1695
1691
  }
1696
1692
  const bodyBytesLimit = this.getService(RouterOptions).requestBodyBytesLimit;
1697
1693
  debug("Fetching a request body.");
@@ -1715,7 +1711,7 @@ function parseJsonBody(input) {
1715
1711
  try {
1716
1712
  return JSON.parse(input);
1717
1713
  } catch (error) {
1718
- throw createError(import_http_errors2.default.BadRequest, error.message);
1714
+ throw new import_http_errors2.default.BadRequest(error.message);
1719
1715
  }
1720
1716
  }
1721
1717
  __name(parseJsonBody, "parseJsonBody");
@@ -2565,7 +2561,6 @@ var TrieRouter = _TrieRouter;
2565
2561
  EXPOSED_ERROR_PROPERTIES,
2566
2562
  ErrorSender,
2567
2563
  HttpMethod,
2568
- METHODS_WITH_BODY,
2569
2564
  QueryParser,
2570
2565
  ROOT_PATH,
2571
2566
  ROUTER_HOOK_TYPES,
@@ -2578,7 +2573,6 @@ var TrieRouter = _TrieRouter;
2578
2573
  RouterHookType,
2579
2574
  RouterOptions,
2580
2575
  TrieRouter,
2581
- UNPARSABLE_MEDIA_TYPES,
2582
2576
  cloneDeep,
2583
2577
  createCookieString,
2584
2578
  createError,
@@ -2587,6 +2581,7 @@ var TrieRouter = _TrieRouter;
2587
2581
  createRouteMock,
2588
2582
  fetchRequestBody,
2589
2583
  getRequestPathname,
2584
+ hasRequestBody,
2590
2585
  isPromise,
2591
2586
  isReadableStream,
2592
2587
  isResponseSent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/js-trie-router",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "HTTP маршрутизатор для Node.js на основе префиксного дерева",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -2,16 +2,6 @@ import {IncomingMessage} from 'http';
2
2
  import {ValueOrPromise} from '../types.js';
3
3
  import {DebuggableService} from '../debuggable-service.js';
4
4
 
5
- /**
6
- * Method names to be parsed.
7
- */
8
- export declare const METHODS_WITH_BODY: string[];
9
-
10
- /**
11
- * Unparsable media types.
12
- */
13
- export declare const UNPARSABLE_MEDIA_TYPES: string[];
14
-
15
5
  /**
16
6
  * Body parser function.
17
7
  */
@@ -5,25 +5,12 @@ import {DebuggableService} from '../debuggable-service.js';
5
5
 
6
6
  import {
7
7
  createError,
8
+ hasRequestBody,
8
9
  parseContentType,
9
10
  fetchRequestBody,
10
11
  getRequestPathname,
11
12
  } from '../utils/index.js';
12
13
 
13
- /**
14
- * Method names to be parsed.
15
- *
16
- * @type {string[]}
17
- */
18
- export const METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE'];
19
-
20
- /**
21
- * Unparsable media types.
22
- *
23
- * @type {string[]}
24
- */
25
- export const UNPARSABLE_MEDIA_TYPES = ['multipart/form-data'];
26
-
27
14
  /**
28
15
  * Body parser.
29
16
  */
@@ -59,7 +46,7 @@ export class BodyParser extends DebuggableService {
59
46
  parser,
60
47
  );
61
48
  }
62
- this._parsers[mediaType] = parser;
49
+ this._parsers[mediaType.toLowerCase()] = parser;
63
50
  return this;
64
51
  }
65
52
 
@@ -77,7 +64,7 @@ export class BodyParser extends DebuggableService {
77
64
  mediaType,
78
65
  );
79
66
  }
80
- return Boolean(this._parsers[mediaType]);
67
+ return Boolean(this._parsers[mediaType.toLowerCase()]);
81
68
  }
82
69
 
83
70
  /**
@@ -94,7 +81,7 @@ export class BodyParser extends DebuggableService {
94
81
  mediaType,
95
82
  );
96
83
  }
97
- const parser = this._parsers[mediaType];
84
+ const parser = this._parsers[mediaType.toLowerCase()];
98
85
  if (!parser) {
99
86
  throw new InvalidArgumentError(
100
87
  'Media type %v does not have a parser.',
@@ -118,7 +105,7 @@ export class BodyParser extends DebuggableService {
118
105
  mediaType,
119
106
  );
120
107
  }
121
- delete this._parsers[mediaType];
108
+ delete this._parsers[mediaType.toLowerCase()];
122
109
  return this;
123
110
  }
124
111
 
@@ -135,17 +122,11 @@ export class BodyParser extends DebuggableService {
135
122
  request.method.toUpperCase(),
136
123
  getRequestPathname(request),
137
124
  );
138
- if (!METHODS_WITH_BODY.includes(request.method.toUpperCase())) {
139
- debug(
140
- 'Skipping body parsing for the %s method.',
141
- request.method.toUpperCase(),
142
- );
125
+ if (!hasRequestBody(request)) {
126
+ debug('Skipping body parsing because no body is provided.');
143
127
  return;
144
128
  }
145
- const contentType = (request.headers['content-type'] || '').replace(
146
- /^([^;]+);.*$/,
147
- '$1',
148
- );
129
+ const contentType = request.headers['content-type'];
149
130
  if (!contentType) {
150
131
  debug('Skipping body parsing because no content type is provided.');
151
132
  return;
@@ -157,17 +138,10 @@ export class BodyParser extends DebuggableService {
157
138
  'Unable to parse the "content-type" header.',
158
139
  );
159
140
  }
160
- const parser = this._parsers[mediaType];
141
+ const parser = this._parsers[mediaType.toLowerCase()];
161
142
  if (!parser) {
162
- if (UNPARSABLE_MEDIA_TYPES.includes(mediaType)) {
163
- debug('Skipping body parsing for the media type %v.', mediaType);
164
- return;
165
- }
166
- throw createError(
167
- HttpErrors.UnsupportedMediaType,
168
- 'Media type %v is not supported.',
169
- mediaType,
170
- );
143
+ debug('No body parser for the media type %v.', mediaType);
144
+ return;
171
145
  }
172
146
  const bodyBytesLimit = this.getService(RouterOptions).requestBodyBytesLimit;
173
147
  debug('Fetching a request body.');
@@ -196,6 +170,6 @@ export function parseJsonBody(input) {
196
170
  try {
197
171
  return JSON.parse(input);
198
172
  } catch (error) {
199
- throw createError(HttpErrors.BadRequest, error.message);
173
+ throw new HttpErrors.BadRequest(error.message);
200
174
  }
201
175
  }
@@ -1,16 +1,11 @@
1
1
  import {expect} from 'chai';
2
2
  import HttpErrors from 'http-errors';
3
3
  import {format} from '@e22m4u/js-format';
4
+ import {BodyParser} from './body-parser.js';
4
5
  import {HttpMethod} from '../route/index.js';
5
6
  import {RouterOptions} from '../router-options.js';
6
7
  import {createRequestMock} from '../utils/index.js';
7
8
 
8
- import {
9
- BodyParser,
10
- METHODS_WITH_BODY,
11
- UNPARSABLE_MEDIA_TYPES,
12
- } from './body-parser.js';
13
-
14
9
  describe('BodyParser', function () {
15
10
  describe('defineParser', function () {
16
11
  it('should require the parameter "mediaType" to be a non-empty String', function () {
@@ -106,6 +101,13 @@ describe('BodyParser', function () {
106
101
  S.defineParser(mediaType, parser);
107
102
  expect(S.hasParser(mediaType)).to.be.true;
108
103
  });
104
+
105
+ it('should be case-insensitive when looking up the parser', function () {
106
+ const S = new BodyParser();
107
+ const parser = v => v;
108
+ S.defineParser('MeDiA/TyPe', parser);
109
+ expect(S.hasParser('mEdIa/tYpE')).to.be.true;
110
+ });
109
111
  });
110
112
 
111
113
  describe('getParser', function () {
@@ -147,6 +149,13 @@ describe('BodyParser', function () {
147
149
  S.defineParser(mediaType, parser);
148
150
  expect(S.getParser(mediaType)).to.be.eq(parser);
149
151
  });
152
+
153
+ it('should be case-insensitive when looking up the parser', function () {
154
+ const S = new BodyParser();
155
+ const parser = v => v;
156
+ S.defineParser('MeDiA/TyPe', parser);
157
+ expect(S.getParser('mEdIa/tYpE')).to.be.eq(parser);
158
+ });
150
159
  });
151
160
 
152
161
  describe('removeParser', function () {
@@ -188,71 +197,77 @@ describe('BodyParser', function () {
188
197
  S.removeParser(mediaType);
189
198
  expect(S.hasParser(mediaType)).to.be.false;
190
199
  });
191
- });
192
200
 
193
- describe('parse', function () {
194
- it('should return undefined when the request method is not supported', async function () {
201
+ it('should be case-insensitive when removing the parser', function () {
195
202
  const S = new BodyParser();
196
- const req = createRequestMock({
197
- method: 'unsupported',
198
- body: 'Lorem Ipsum is simply dummy text.',
199
- });
200
- const result = await S.parse(req);
201
- expect(result).to.be.undefined;
202
- });
203
-
204
- it('should return undefined when the request method is not supported even if the "content-type" header is specified', async function () {
205
- const S = new BodyParser();
206
- const req = createRequestMock({
207
- method: 'unsupported',
208
- headers: {'content-type': 'text/plain'},
209
- body: 'Lorem Ipsum is simply dummy text.',
210
- });
211
- const result = await S.parse(req);
212
- expect(result).to.be.undefined;
203
+ const parser = v => v;
204
+ const mediaType = 'MeDiA/TyPe';
205
+ S.defineParser(mediaType, parser);
206
+ expect(S.hasParser(mediaType)).to.be.true;
207
+ S.removeParser('mEdIa/tYpE');
208
+ expect(S.hasParser(mediaType)).to.be.false;
213
209
  });
210
+ });
214
211
 
215
- it('should return undefined when no "content-type" header is specified', async function () {
212
+ describe('parse', function () {
213
+ it('should parse the request body when the "content-type" and "content-length" headers are provided', async function () {
216
214
  const S = new BodyParser();
217
- const req = createRequestMock({method: HttpMethod.POST});
218
- const result = await S.parse(req);
219
- expect(result).to.be.undefined;
215
+ const body = 'Lorem Ipsum is simply dummy text.';
216
+ const headers = {
217
+ 'content-type': 'text/plain',
218
+ 'content-length': Buffer.byteLength(body, 'utf-8'),
219
+ };
220
+ for await (const method of Object.values(HttpMethod)) {
221
+ const req = createRequestMock({method, body, headers});
222
+ const result = await S.parse(req);
223
+ expect(result).to.be.eq(body);
224
+ }
220
225
  });
221
226
 
222
- it('should return undefined when the media type is excluded', async function () {
227
+ it('should parse the request body when the "content-type" and "transfer-encoding" headers are provided', async function () {
223
228
  const S = new BodyParser();
224
- for await (const mediaType of UNPARSABLE_MEDIA_TYPES) {
225
- const req = createRequestMock({
226
- method: HttpMethod.POST,
227
- headers: {'content-type': mediaType},
228
- body: 'Lorem Ipsum is simply dummy text.',
229
- });
229
+ const body = 'Lorem Ipsum is simply dummy text.';
230
+ const headers = {
231
+ 'content-type': 'text/plain',
232
+ 'transfer-encoding': 'chunked',
233
+ };
234
+ for await (const method of Object.values(HttpMethod)) {
235
+ const req = createRequestMock({method, body, headers});
230
236
  const result = await S.parse(req);
231
- expect(result).to.be.undefined;
237
+ expect(result).to.be.eq(body);
232
238
  }
233
239
  });
234
240
 
235
- it('should parse the request body for available methods', async function () {
241
+ it('should skip parsing when the header "content-length" has an invalid value', async function () {
236
242
  const S = new BodyParser();
237
243
  const body = 'Lorem Ipsum is simply dummy text.';
238
- const headers = {'content-type': 'text/plain'};
239
- for await (const method of Object.values(METHODS_WITH_BODY)) {
244
+ const headers = {
245
+ 'content-type': 'text/plain',
246
+ 'content-length': 'invalid',
247
+ };
248
+ for await (const method of Object.values(HttpMethod)) {
240
249
  const req = createRequestMock({method, body, headers});
241
250
  const result = await S.parse(req);
242
- expect(result).to.be.eq(body);
251
+ expect(result).to.be.undefined;
243
252
  }
244
253
  });
245
254
 
246
- it('should throw an error when the media type is not supported', function () {
255
+ it('should return undefined when no "content-type" header is provided', async function () {
256
+ const S = new BodyParser();
257
+ const req = createRequestMock({method: HttpMethod.POST});
258
+ const result = await S.parse(req);
259
+ expect(result).to.be.undefined;
260
+ });
261
+
262
+ it('should return undefined when the media type does not have a registered parser', async function () {
247
263
  const S = new BodyParser();
248
264
  const req = createRequestMock({
249
265
  method: HttpMethod.POST,
250
- headers: {'content-type': 'media/unknown'},
266
+ headers: {'content-type': 'type/unknown'},
267
+ body: 'Lorem Ipsum is simply dummy text.',
251
268
  });
252
- const throwable = () => S.parse(req);
253
- expect(throwable).to.throw(
254
- 'Media type "media/unknown" is not supported.',
255
- );
269
+ const result = await S.parse(req);
270
+ expect(result).to.be.undefined;
256
271
  });
257
272
 
258
273
  it('should use the option "bodyBytesLimit" from the RouterOptions', async function () {