Просмотр исходного кода

fix: pre-handler hooks invocation and request body encoding

e22m4u 2 месяцев назад
Родитель
Сommit
4aa9d4caa5
68 измененных файлов с 685 добавлено и 603 удалено
  1. 191 188
      dist/cjs/index.cjs
  2. 8 8
      examples/cookies-parsing-example.js
  3. 11 7
      examples/params-parsing-example.js
  4. 11 7
      examples/query-parsing-example.js
  5. 4 4
      examples/uptime-example.js
  6. 7 6
      package.json
  7. 2 10
      src/debuggable-service.d.ts
  8. 16 17
      src/debuggable-service.js
  9. 3 4
      src/hooks/hook-invoker.js
  10. 3 3
      src/hooks/hook-invoker.spec.js
  11. 1 1
      src/hooks/hook-registry.d.ts
  12. 6 7
      src/hooks/hook-registry.js
  13. 14 5
      src/hooks/hook-registry.spec.js
  14. 10 11
      src/parsers/body-parser.js
  15. 4 4
      src/parsers/body-parser.spec.js
  16. 0 15
      src/parsers/cookie-parser.d.ts
  17. 0 32
      src/parsers/cookie-parser.js
  18. 15 0
      src/parsers/cookies-parser.d.ts
  19. 33 0
      src/parsers/cookies-parser.js
  20. 6 6
      src/parsers/cookies-parser.spec.js
  21. 1 1
      src/parsers/index.d.ts
  22. 1 1
      src/parsers/index.js
  23. 4 3
      src/parsers/query-parser.js
  24. 2 2
      src/parsers/request-parser.d.ts
  25. 7 7
      src/parsers/request-parser.js
  26. 12 12
      src/parsers/request-parser.spec.js
  27. 3 3
      src/request-context.d.ts
  28. 5 5
      src/request-context.js
  29. 3 3
      src/request-context.spec.js
  30. 14 14
      src/route-registry.js
  31. 4 1
      src/route-registry.spec.js
  32. 12 7
      src/route.d.ts
  33. 16 14
      src/route.js
  34. 13 7
      src/route.spec.js
  35. 1 1
      src/router-options.js
  36. 1 1
      src/router-options.spec.js
  37. 6 5
      src/senders/data-sender.js
  38. 6 4
      src/senders/error-sender.js
  39. 24 14
      src/trie-router.js
  40. 35 5
      src/trie-router.spec.js
  41. 0 6
      src/utils/create-cookie-string.d.ts
  42. 6 0
      src/utils/create-cookies-string.d.ts
  43. 4 4
      src/utils/create-cookies-string.js
  44. 7 7
      src/utils/create-cookies-string.spec.js
  45. 1 1
      src/utils/create-debugger.js
  46. 1 1
      src/utils/create-debugger.spec.js
  47. 2 2
      src/utils/create-error.js
  48. 2 2
      src/utils/create-error.spec.js
  49. 1 1
      src/utils/create-request-mock.d.ts
  50. 47 43
      src/utils/create-request-mock.js
  51. 18 18
      src/utils/create-request-mock.spec.js
  52. 11 2
      src/utils/fetch-request-body.d.ts
  53. 10 17
      src/utils/fetch-request-body.js
  54. 8 9
      src/utils/fetch-request-body.spec.js
  55. 1 1
      src/utils/get-request-pathname.js
  56. 1 1
      src/utils/get-request-pathname.spec.js
  57. 2 2
      src/utils/index.d.ts
  58. 2 2
      src/utils/index.js
  59. 1 2
      src/utils/is-readable-stream.js
  60. 1 1
      src/utils/is-response-sent.js
  61. 1 1
      src/utils/is-response-sent.spec.js
  62. 1 1
      src/utils/parse-content-type.js
  63. 0 19
      src/utils/parse-cookie.d.ts
  64. 19 0
      src/utils/parse-cookies.d.ts
  65. 8 5
      src/utils/parse-cookies.js
  66. 13 8
      src/utils/parse-cookies.spec.js
  67. 1 1
      src/utils/to-camel-case.js
  68. 1 1
      src/utils/to-camel-case.spec.js

Разница между файлами не показана из-за своего большого размера
+ 191 - 188
dist/cjs/index.cjs


+ 8 - 8
examples/cookie-parsing-example.js → examples/cookies-parsing-example.js

@@ -4,25 +4,25 @@ import {HttpMethod} from '../src/route.js';
 
 
 const router = new TrieRouter();
 const router = new TrieRouter();
 
 
-// регистрация роута для вывода
-// переданных Cookie
+// регистрация маршрута для разбора
+// передаваемого заголовка "Cookie"
 router.defineRoute({
 router.defineRoute({
   method: HttpMethod.GET,
   method: HttpMethod.GET,
-  path: '/showCookie',
-  handler: ({cookie}) => cookie,
+  path: '/parseCookies',
+  handler: ({cookies}) => cookies,
 });
 });
 
 
-// создаем экземпляр HTTP сервера
-// и подключаем обработчик запросов
+// создание экземпляра HTTP сервера
+// и подключение обработчика запросов
 const server = new http.Server();
 const server = new http.Server();
 server.on('request', router.requestListener);
 server.on('request', router.requestListener);
 
 
-// слушаем входящие запросы
+// прослушивание входящих запросов
 // на указанный адрес и порт
 // на указанный адрес и порт
 const port = 3000;
 const port = 3000;
 const host = '0.0.0.0';
 const host = '0.0.0.0';
 server.listen(port, host, function () {
 server.listen(port, host, function () {
   const cyan = '\x1b[36m%s\x1b[0m';
   const cyan = '\x1b[36m%s\x1b[0m';
   console.log(cyan, 'Server listening on port:', port);
   console.log(cyan, 'Server listening on port:', port);
-  console.log(cyan, 'Open in browser:', `http://${host}:${port}/showCookie`);
+  console.log(cyan, 'Open in browser:', `http://${host}:${port}/parseCookies`);
 });
 });

+ 11 - 7
examples/params-parsing-example.js

@@ -4,25 +4,29 @@ import {HttpMethod} from '../src/route.js';
 
 
 const router = new TrieRouter();
 const router = new TrieRouter();
 
 
-// регистрация роута для вывода
-// переданных параметров пути
+// регистрация маршрута для разбора
+// передаваемых параметров пути
 router.defineRoute({
 router.defineRoute({
   method: HttpMethod.GET,
   method: HttpMethod.GET,
-  path: '/showParams/:p1/:p2',
+  path: '/parseParams/:p1/:p2',
   handler: ({params}) => params,
   handler: ({params}) => params,
 });
 });
 
 
-// создаем экземпляр HTTP сервера
-// и подключаем обработчик запросов
+// создание экземпляра HTTP сервера
+// и подключение обработчика запросов
 const server = new http.Server();
 const server = new http.Server();
 server.on('request', router.requestListener);
 server.on('request', router.requestListener);
 
 
-// слушаем входящие запросы
+// прослушивание входящих запросов
 // на указанный адрес и порт
 // на указанный адрес и порт
 const port = 3000;
 const port = 3000;
 const host = '0.0.0.0';
 const host = '0.0.0.0';
 server.listen(port, host, function () {
 server.listen(port, host, function () {
   const cyan = '\x1b[36m%s\x1b[0m';
   const cyan = '\x1b[36m%s\x1b[0m';
   console.log(cyan, 'Server listening on port:', port);
   console.log(cyan, 'Server listening on port:', port);
-  console.log(cyan, 'Open in browser:', `http://${host}:${port}/showParams/foo/bar`);
+  console.log(
+    cyan,
+    'Open in browser:',
+    `http://${host}:${port}/parseParams/foo/bar`,
+  );
 });
 });

+ 11 - 7
examples/query-parsing-example.js

@@ -4,25 +4,29 @@ import {HttpMethod} from '../src/route.js';
 
 
 const router = new TrieRouter();
 const router = new TrieRouter();
 
 
-// регистрация роута для вывода
-// переданных "query" параметров
+// регистрация маршрута для разбора
+// передаваемых параметров запроса
 router.defineRoute({
 router.defineRoute({
   method: HttpMethod.GET,
   method: HttpMethod.GET,
-  path: '/showQuery',
+  path: '/parseQuery',
   handler: ({query}) => query,
   handler: ({query}) => query,
 });
 });
 
 
-// создаем экземпляр HTTP сервера
-// и подключаем обработчик запросов
+// создание экземпляра HTTP сервера
+// и подключение обработчика запросов
 const server = new http.Server();
 const server = new http.Server();
 server.on('request', router.requestListener);
 server.on('request', router.requestListener);
 
 
-// слушаем входящие запросы
+// прослушивание входящих запросов
 // на указанный адрес и порт
 // на указанный адрес и порт
 const port = 3000;
 const port = 3000;
 const host = '0.0.0.0';
 const host = '0.0.0.0';
 server.listen(port, host, function () {
 server.listen(port, host, function () {
   const cyan = '\x1b[36m%s\x1b[0m';
   const cyan = '\x1b[36m%s\x1b[0m';
   console.log(cyan, 'Server listening on port:', port);
   console.log(cyan, 'Server listening on port:', port);
-  console.log(cyan, 'Open in browser:', `http://${host}:${port}/showQuery?foo=bar&baz=qux`);
+  console.log(
+    cyan,
+    'Open in browser:',
+    `http://${host}:${port}/parseQuery?foo=bar&baz=qux`,
+  );
 });
 });

+ 4 - 4
examples/uptime-example.js

@@ -4,7 +4,7 @@ import {HttpMethod} from '../src/route.js';
 
 
 const router = new TrieRouter();
 const router = new TrieRouter();
 
 
-// регистрация роута для вывода
+// регистрация маршрута для вывода
 // времени работы сервера
 // времени работы сервера
 router.defineRoute({
 router.defineRoute({
   method: HttpMethod.GET,
   method: HttpMethod.GET,
@@ -24,12 +24,12 @@ router.defineRoute({
   },
   },
 })
 })
 
 
-// создаем экземпляр HTTP сервера
-// и подключаем обработчик запросов
+// создание экземпляра HTTP сервера
+// и подключение обработчика запросов
 const server = new http.Server();
 const server = new http.Server();
 server.on('request', router.requestListener);
 server.on('request', router.requestListener);
 
 
-// слушаем входящие запросы
+// прослушивание входящих запросов
 // на указанный адрес и порт
 // на указанный адрес и порт
 const port = 3000;
 const port = 3000;
 const host = '0.0.0.0';
 const host = '0.0.0.0';

+ 7 - 6
package.json

@@ -38,30 +38,31 @@
     "prepare": "husky"
     "prepare": "husky"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@e22m4u/js-debug": "~0.3.1",
     "@e22m4u/js-format": "~0.2.0",
     "@e22m4u/js-format": "~0.2.0",
     "@e22m4u/js-path-trie": "~0.0.11",
     "@e22m4u/js-path-trie": "~0.0.11",
-    "@e22m4u/js-service": "~0.3.8",
+    "@e22m4u/js-service": "~0.4.0",
     "debug": "~4.4.3",
     "debug": "~4.4.3",
     "http-errors": "~2.0.0",
     "http-errors": "~2.0.0",
     "statuses": "~2.0.2"
     "statuses": "~2.0.2"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@commitlint/cli": "~19.8.1",
-    "@commitlint/config-conventional": "~19.8.1",
+    "@commitlint/cli": "~20.1.0",
+    "@commitlint/config-conventional": "~20.0.0",
     "@eslint/js": "~9.36.0",
     "@eslint/js": "~9.36.0",
     "@types/chai-as-promised": "~8.0.2",
     "@types/chai-as-promised": "~8.0.2",
     "c8": "~10.1.3",
     "c8": "~10.1.3",
-    "chai": "~6.0.1",
+    "chai": "~6.2.0",
     "chai-as-promised": "~8.0.2",
     "chai-as-promised": "~8.0.2",
     "esbuild": "~0.25.10",
     "esbuild": "~0.25.10",
     "eslint": "~9.36.0",
     "eslint": "~9.36.0",
     "eslint-config-prettier": "~10.1.8",
     "eslint-config-prettier": "~10.1.8",
     "eslint-plugin-chai-expect": "~3.1.0",
     "eslint-plugin-chai-expect": "~3.1.0",
-    "eslint-plugin-jsdoc": "~60.3.0",
+    "eslint-plugin-jsdoc": "~60.5.0",
     "eslint-plugin-mocha": "~11.1.0",
     "eslint-plugin-mocha": "~11.1.0",
     "globals": "~16.4.0",
     "globals": "~16.4.0",
     "husky": "~9.1.7",
     "husky": "~9.1.7",
-    "mocha": "~11.7.2",
+    "mocha": "~11.7.3",
     "prettier": "~3.6.2",
     "prettier": "~3.6.2",
     "rimraf": "~6.0.1",
     "rimraf": "~6.0.1",
     "typescript": "~5.9.2"
     "typescript": "~5.9.2"

+ 2 - 10
src/debuggable-service.d.ts

@@ -1,14 +1,6 @@
-import {Debugger} from './utils/index.js';
-import {Service} from '@e22m4u/js-service';
+import {DebuggableService as BaseDebuggableService} from '@e22m4u/js-service';
 
 
 /**
 /**
  * Debuggable service.
  * Debuggable service.
  */
  */
-declare class DebuggableService extends Service {
-  /**
-   * Debug.
-   *
-   * @protected
-   */
-  protected debug: Debugger;
-}
+declare class DebuggableService extends BaseDebuggableService {}

+ 16 - 17
src/debuggable-service.js

@@ -1,28 +1,27 @@
-import {Service} from '@e22m4u/js-service';
-import {toCamelCase} from './utils/index.js';
-import {createDebugger} from './utils/index.js';
-import {ServiceContainer} from '@e22m4u/js-service';
+import {DebuggableService as BaseDebuggableService} from '@e22m4u/js-service';
 
 
 /**
 /**
- * Debuggable service.
+ * @typedef {import('@e22m4u/js-service').ServiceContainer} ServiceContainer
  */
  */
-export class DebuggableService extends Service {
-  /**
-   * Debug.
-   *
-   * @type {Function}
-   */
-  debug;
 
 
+/**
+ * Module debug namespace.
+ */
+export const MODULE_DEBUG_NAMESPACE = 'jsTrieRouter';
+
+/**
+ * Debuggable service.
+ */
+export class DebuggableService extends BaseDebuggableService {
   /**
   /**
    * Constructor.
    * Constructor.
    *
    *
    * @param {ServiceContainer} container
    * @param {ServiceContainer} container
    */
    */
-  constructor(container) {
-    super(container);
-    const serviceName = toCamelCase(this.constructor.name);
-    this.debug = createDebugger(serviceName);
-    this.debug('The %v is created.', this.constructor);
+  constructor(container = undefined) {
+    super(container, {
+      namespace: MODULE_DEBUG_NAMESPACE,
+      noEnvironmentNamespace: true,
+    });
   }
   }
 }
 }

+ 3 - 4
src/hooks/hook-invoker.js

@@ -24,14 +24,14 @@ export class HookInvoker extends DebuggableService {
       throw new Errorf(
       throw new Errorf(
         'The parameter "route" of ' +
         'The parameter "route" of ' +
           'the HookInvoker.invokeAndContinueUntilValueReceived ' +
           'the HookInvoker.invokeAndContinueUntilValueReceived ' +
-          'should be a Route instance, but %v given.',
+          'should be a Route instance, but %v was given.',
         route,
         route,
       );
       );
     if (!hookType || typeof hookType !== 'string')
     if (!hookType || typeof hookType !== 'string')
       throw new Errorf(
       throw new Errorf(
         'The parameter "hookType" of ' +
         'The parameter "hookType" of ' +
           'the HookInvoker.invokeAndContinueUntilValueReceived ' +
           'the HookInvoker.invokeAndContinueUntilValueReceived ' +
-          'should be a non-empty String, but %v given.',
+          'should be a non-empty String, but %v was given.',
         hookType,
         hookType,
       );
       );
     if (!Object.values(HookType).includes(hookType))
     if (!Object.values(HookType).includes(hookType))
@@ -45,7 +45,7 @@ export class HookInvoker extends DebuggableService {
       throw new Errorf(
       throw new Errorf(
         'The parameter "response" of ' +
         'The parameter "response" of ' +
           'the HookInvoker.invokeAndContinueUntilValueReceived ' +
           'the HookInvoker.invokeAndContinueUntilValueReceived ' +
-          'should be a ServerResponse instance, but %v given.',
+          'should be a ServerResponse instance, but %v was given.',
         response,
         response,
       );
       );
     }
     }
@@ -82,7 +82,6 @@ export class HookInvoker extends DebuggableService {
           // если ответ уже был отправлен,
           // если ответ уже был отправлен,
           // то останавливаем выполнение
           // то останавливаем выполнение
           if (isResponseSent(response)) {
           if (isResponseSent(response)) {
-            result = response;
             return;
             return;
           }
           }
           // если предыдущий Promise вернул значение
           // если предыдущий Promise вернул значение

+ 3 - 3
src/hooks/hook-invoker.spec.js

@@ -18,7 +18,7 @@ describe('HookInvoker', function () {
         format(
         format(
           'The parameter "route" of ' +
           'The parameter "route" of ' +
             'the HookInvoker.invokeAndContinueUntilValueReceived ' +
             'the HookInvoker.invokeAndContinueUntilValueReceived ' +
-            'should be a Route instance, but %s given.',
+            'should be a Route instance, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -54,7 +54,7 @@ describe('HookInvoker', function () {
         format(
         format(
           'The parameter "hookType" of ' +
           'The parameter "hookType" of ' +
             'the HookInvoker.invokeAndContinueUntilValueReceived ' +
             'the HookInvoker.invokeAndContinueUntilValueReceived ' +
-            'should be a non-empty String, but %s given.',
+            'should be a non-empty String, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
@@ -98,7 +98,7 @@ describe('HookInvoker', function () {
         format(
         format(
           'The parameter "response" of ' +
           'The parameter "response" of ' +
             'the HookInvoker.invokeAndContinueUntilValueReceived ' +
             'the HookInvoker.invokeAndContinueUntilValueReceived ' +
-            'should be a ServerResponse instance, but %s given.',
+            'should be a ServerResponse instance, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));

+ 1 - 1
src/hooks/hook-registry.d.ts

@@ -37,7 +37,7 @@ export declare class HookRegistry extends DebuggableService {
    * @param type
    * @param type
    * @param hook
    * @param hook
    */
    */
-  hasHook(type: HookType, hook: RouterHook): this;
+  hasHook(type: HookType, hook: RouterHook): boolean;
 
 
   /**
   /**
    * Get hooks.
    * Get hooks.

+ 6 - 7
src/hooks/hook-registry.js

@@ -1,5 +1,4 @@
 import {Errorf} from '@e22m4u/js-format';
 import {Errorf} from '@e22m4u/js-format';
-import {DebuggableService} from '../debuggable-service.js';
 
 
 /**
 /**
  * Hook type.
  * Hook type.
@@ -17,7 +16,7 @@ export const HookType = {
 /**
 /**
  * Hook registry.
  * Hook registry.
  */
  */
-export class HookRegistry extends DebuggableService {
+export class HookRegistry {
   /**
   /**
    * Hooks.
    * Hooks.
    *
    *
@@ -35,12 +34,12 @@ export class HookRegistry extends DebuggableService {
    */
    */
   addHook(type, hook) {
   addHook(type, hook) {
     if (!type || typeof type !== 'string')
     if (!type || typeof type !== 'string')
-      throw new Errorf('The hook type is required, but %v given.', type);
+      throw new Errorf('The hook type is required, but %v was given.', type);
     if (!Object.values(HookType).includes(type))
     if (!Object.values(HookType).includes(type))
       throw new Errorf('The hook type %v is not supported.', type);
       throw new Errorf('The hook type %v is not supported.', type);
     if (!hook || typeof hook !== 'function')
     if (!hook || typeof hook !== 'function')
       throw new Errorf(
       throw new Errorf(
-        'The hook %v should be a Function, but %v given.',
+        'The hook %v should be a Function, but %v was given.',
         type,
         type,
         hook,
         hook,
       );
       );
@@ -59,12 +58,12 @@ export class HookRegistry extends DebuggableService {
    */
    */
   hasHook(type, hook) {
   hasHook(type, hook) {
     if (!type || typeof type !== 'string')
     if (!type || typeof type !== 'string')
-      throw new Errorf('The hook type is required, but %v given.', type);
+      throw new Errorf('The hook type is required, but %v was given.', type);
     if (!Object.values(HookType).includes(type))
     if (!Object.values(HookType).includes(type))
       throw new Errorf('The hook type %v is not supported.', type);
       throw new Errorf('The hook type %v is not supported.', type);
     if (!hook || typeof hook !== 'function')
     if (!hook || typeof hook !== 'function')
       throw new Errorf(
       throw new Errorf(
-        'The hook %v should be a Function, but %v given.',
+        'The hook %v should be a Function, but %v was given.',
         type,
         type,
         hook,
         hook,
       );
       );
@@ -80,7 +79,7 @@ export class HookRegistry extends DebuggableService {
    */
    */
   getHooks(type) {
   getHooks(type) {
     if (!type || typeof type !== 'string')
     if (!type || typeof type !== 'string')
-      throw new Errorf('The hook type is required, but %v given.', type);
+      throw new Errorf('The hook type is required, but %v was given.', type);
     if (!Object.values(HookType).includes(type))
     if (!Object.values(HookType).includes(type))
       throw new Errorf('The hook type %v is not supported.', type);
       throw new Errorf('The hook type %v is not supported.', type);
     return this._hooks.get(type) || [];
     return this._hooks.get(type) || [];

+ 14 - 5
src/hooks/hook-registry.spec.js

@@ -8,7 +8,8 @@ describe('HookRegistry', function () {
     it('requires the parameter "type" to be a non-empty String', function () {
     it('requires the parameter "type" to be a non-empty String', function () {
       const s = new HookRegistry();
       const s = new HookRegistry();
       const throwable = v => () => s.addHook(v, () => undefined);
       const throwable = v => () => s.addHook(v, () => undefined);
-      const error = v => format('The hook type is required, but %s given.', v);
+      const error = v =>
+        format('The hook type is required, but %s was given.', v);
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(0)).to.throw(error('0'));
       expect(throwable(0)).to.throw(error('0'));
@@ -26,7 +27,10 @@ describe('HookRegistry', function () {
       const s = new HookRegistry();
       const s = new HookRegistry();
       const throwable = v => () => s.addHook(HookType.PRE_HANDLER, v);
       const throwable = v => () => s.addHook(HookType.PRE_HANDLER, v);
       const error = v =>
       const error = v =>
-        format('The hook "preHandler" should be a Function, but %s given.', v);
+        format(
+          'The hook "preHandler" should be a Function, but %s was given.',
+          v,
+        );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));
@@ -69,7 +73,8 @@ describe('HookRegistry', function () {
     it('requires the parameter "type" to be a non-empty String', function () {
     it('requires the parameter "type" to be a non-empty String', function () {
       const s = new HookRegistry();
       const s = new HookRegistry();
       const throwable = v => () => s.hasHook(v, () => undefined);
       const throwable = v => () => s.hasHook(v, () => undefined);
-      const error = v => format('The hook type is required, but %s given.', v);
+      const error = v =>
+        format('The hook type is required, but %s was given.', v);
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(0)).to.throw(error('0'));
       expect(throwable(0)).to.throw(error('0'));
@@ -87,7 +92,10 @@ describe('HookRegistry', function () {
       const s = new HookRegistry();
       const s = new HookRegistry();
       const throwable = v => () => s.hasHook(HookType.PRE_HANDLER, v);
       const throwable = v => () => s.hasHook(HookType.PRE_HANDLER, v);
       const error = v =>
       const error = v =>
-        format('The hook "preHandler" should be a Function, but %s given.', v);
+        format(
+          'The hook "preHandler" should be a Function, but %s was given.',
+          v,
+        );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));
@@ -123,7 +131,8 @@ describe('HookRegistry', function () {
     it('requires the parameter "type" to be a non-empty String', function () {
     it('requires the parameter "type" to be a non-empty String', function () {
       const s = new HookRegistry();
       const s = new HookRegistry();
       const throwable = v => () => s.getHooks(v);
       const throwable = v => () => s.getHooks(v);
-      const error = v => format('The hook type is required, but %s given.', v);
+      const error = v =>
+        format('The hook type is required, but %s was given.', v);
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(0)).to.throw(error('0'));
       expect(throwable(0)).to.throw(error('0'));

+ 10 - 11
src/parsers/body-parser.js

@@ -45,13 +45,13 @@ export class BodyParser extends DebuggableService {
     if (!mediaType || typeof mediaType !== 'string')
     if (!mediaType || typeof mediaType !== 'string')
       throw new Errorf(
       throw new Errorf(
         'The parameter "mediaType" of BodyParser.defineParser ' +
         'The parameter "mediaType" of BodyParser.defineParser ' +
-          'should be a non-empty String, but %v given.',
+          'should be a non-empty String, but %v was given.',
         mediaType,
         mediaType,
       );
       );
     if (!parser || typeof parser !== 'function')
     if (!parser || typeof parser !== 'function')
       throw new Errorf(
       throw new Errorf(
         'The parameter "parser" of BodyParser.defineParser ' +
         'The parameter "parser" of BodyParser.defineParser ' +
-          'should be a Function, but %v given.',
+          'should be a Function, but %v was given.',
         parser,
         parser,
       );
       );
     this._parsers[mediaType] = parser;
     this._parsers[mediaType] = parser;
@@ -68,7 +68,7 @@ export class BodyParser extends DebuggableService {
     if (!mediaType || typeof mediaType !== 'string')
     if (!mediaType || typeof mediaType !== 'string')
       throw new Errorf(
       throw new Errorf(
         'The parameter "mediaType" of BodyParser.hasParser ' +
         'The parameter "mediaType" of BodyParser.hasParser ' +
-          'should be a non-empty String, but %v given.',
+          'should be a non-empty String, but %v was given.',
         mediaType,
         mediaType,
       );
       );
     return Boolean(this._parsers[mediaType]);
     return Boolean(this._parsers[mediaType]);
@@ -84,7 +84,7 @@ export class BodyParser extends DebuggableService {
     if (!mediaType || typeof mediaType !== 'string')
     if (!mediaType || typeof mediaType !== 'string')
       throw new Errorf(
       throw new Errorf(
         'The parameter "mediaType" of BodyParser.deleteParser ' +
         'The parameter "mediaType" of BodyParser.deleteParser ' +
-          'should be a non-empty String, but %v given.',
+          'should be a non-empty String, but %v was given.',
         mediaType,
         mediaType,
       );
       );
     const parser = this._parsers[mediaType];
     const parser = this._parsers[mediaType];
@@ -100,8 +100,9 @@ export class BodyParser extends DebuggableService {
    * @returns {Promise<*>|undefined}
    * @returns {Promise<*>|undefined}
    */
    */
   parse(req) {
   parse(req) {
+    const debug = this.getDebuggerFor(this.parse);
     if (!METHODS_WITH_BODY.includes(req.method.toUpperCase())) {
     if (!METHODS_WITH_BODY.includes(req.method.toUpperCase())) {
-      this.debug(
+      debug(
         'Body parsing was skipped for the %s request.',
         'Body parsing was skipped for the %s request.',
         req.method.toUpperCase(),
         req.method.toUpperCase(),
       );
       );
@@ -112,8 +113,8 @@ export class BodyParser extends DebuggableService {
       '$1',
       '$1',
     );
     );
     if (!contentType) {
     if (!contentType) {
-      this.debug(
-        'Body parsing was skipped because the request has no content type.',
+      debug(
+        'Body parsing was skipped because the request had no content type.',
       );
       );
       return;
       return;
     }
     }
@@ -126,7 +127,7 @@ export class BodyParser extends DebuggableService {
     const parser = this._parsers[mediaType];
     const parser = this._parsers[mediaType];
     if (!parser) {
     if (!parser) {
       if (UNPARSABLE_MEDIA_TYPES.includes(mediaType)) {
       if (UNPARSABLE_MEDIA_TYPES.includes(mediaType)) {
-        this.debug('Body parsing was skipped for %v.', mediaType);
+        debug('Body parsing was skipped for %v.', mediaType);
         return;
         return;
       }
       }
       throw createError(
       throw createError(
@@ -154,8 +155,6 @@ export function parseJsonBody(input) {
   try {
   try {
     return JSON.parse(input);
     return JSON.parse(input);
   } catch (error) {
   } catch (error) {
-    if (process.env['DEBUG'] || process.env['NODE_ENV'] === 'development')
-      console.warn(error);
-    throw createError(HttpErrors.BadRequest, 'Unable to parse request body.');
+    throw createError(HttpErrors.BadRequest, error.message);
   }
   }
 }
 }

+ 4 - 4
src/parsers/body-parser.spec.js

@@ -16,7 +16,7 @@ describe('BodyParser', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "mediaType" of BodyParser.defineParser ' +
           'The parameter "mediaType" of BodyParser.defineParser ' +
-            'should be a non-empty String, but %s given.',
+            'should be a non-empty String, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
@@ -38,7 +38,7 @@ describe('BodyParser', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "parser" of BodyParser.defineParser ' +
           'The parameter "parser" of BodyParser.defineParser ' +
-            'should be a Function, but %s given.',
+            'should be a Function, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -76,7 +76,7 @@ describe('BodyParser', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "mediaType" of BodyParser.hasParser ' +
           'The parameter "mediaType" of BodyParser.hasParser ' +
-            'should be a non-empty String, but %s given.',
+            'should be a non-empty String, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
@@ -111,7 +111,7 @@ describe('BodyParser', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "mediaType" of BodyParser.deleteParser ' +
           'The parameter "mediaType" of BodyParser.deleteParser ' +
-            'should be a non-empty String, but %s given.',
+            'should be a non-empty String, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));

+ 0 - 15
src/parsers/cookie-parser.d.ts

@@ -1,15 +0,0 @@
-import {IncomingMessage} from 'http';
-import {ParsedCookie} from '../utils/index.js';
-import {DebuggableService} from '../debuggable-service.js';
-
-/**
- * Cookie parser.
- */
-export declare class CookieParser extends DebuggableService {
-  /**
-   * Parse.
-   *
-   * @param req
-   */
-  parse(req: IncomingMessage): ParsedCookie;
-}

+ 0 - 32
src/parsers/cookie-parser.js

@@ -1,32 +0,0 @@
-import {parseCookie} from '../utils/index.js';
-import {getRequestPathname} from '../utils/index.js';
-import {DebuggableService} from '../debuggable-service.js';
-
-/**
- * Cookie parser.
- */
-export class CookieParser extends DebuggableService {
-  /**
-   * Parse
-   *
-   * @param {import('http').IncomingMessage} req
-   * @returns {object}
-   */
-  parse(req) {
-    const cookieString = req.headers['cookie'] || '';
-    const cookie = parseCookie(cookieString);
-    const cookieKeys = Object.keys(cookie);
-    if (cookieKeys.length) {
-      cookieKeys.forEach(key => {
-        this.debug('The cookie %v has the value %v.', key, cookie[key]);
-      });
-    } else {
-      this.debug(
-        'The request %s %v has no cookie.',
-        req.method,
-        getRequestPathname(req),
-      );
-    }
-    return cookie;
-  }
-}

+ 15 - 0
src/parsers/cookies-parser.d.ts

@@ -0,0 +1,15 @@
+import {IncomingMessage} from 'http';
+import {ParsedCookies} from '../utils/index.js';
+import {DebuggableService} from '../debuggable-service.js';
+
+/**
+ * Cookies parser.
+ */
+export declare class CookiesParser extends DebuggableService {
+  /**
+   * Parse.
+   *
+   * @param req
+   */
+  parse(req: IncomingMessage): ParsedCookies;
+}

+ 33 - 0
src/parsers/cookies-parser.js

@@ -0,0 +1,33 @@
+import {parseCookies} from '../utils/index.js';
+import {getRequestPathname} from '../utils/index.js';
+import {DebuggableService} from '../debuggable-service.js';
+
+/**
+ * Cookies parser.
+ */
+export class CookiesParser extends DebuggableService {
+  /**
+   * Parse
+   *
+   * @param {import('http').IncomingMessage} req
+   * @returns {object}
+   */
+  parse(req) {
+    const debug = this.getDebuggerFor(this.parse);
+    const cookiesString = req.headers['cookie'] || '';
+    const cookies = parseCookies(cookiesString);
+    const cookiesKeys = Object.keys(cookies);
+    if (cookiesKeys.length) {
+      cookiesKeys.forEach(key => {
+        debug('The cookie %v had the value %v.', key, cookies[key]);
+      });
+    } else {
+      debug(
+        'The request %s %v had no cookies.',
+        req.method,
+        getRequestPathname(req),
+      );
+    }
+    return cookies;
+  }
+}

+ 6 - 6
src/parsers/cookie-parser.spec.js → src/parsers/cookies-parser.spec.js

@@ -1,10 +1,10 @@
 import {expect} from '../chai.js';
 import {expect} from '../chai.js';
-import {CookieParser} from './cookie-parser.js';
+import {CookiesParser} from './cookies-parser.js';
 
 
-describe('CookieParser', function () {
+describe('CookiesParser', function () {
   describe('parse', function () {
   describe('parse', function () {
-    it('returns cookie parameters', function () {
-      const parser = new CookieParser();
+    it('returns parsed cookies as a plain object', function () {
+      const parser = new CookiesParser();
       const value = 'pkg=math; equation=E%3Dmc%5E2';
       const value = 'pkg=math; equation=E%3Dmc%5E2';
       const result = parser.parse({url: '', headers: {cookie: value}});
       const result = parser.parse({url: '', headers: {cookie: value}});
       expect(result).to.have.property('pkg', 'math');
       expect(result).to.have.property('pkg', 'math');
@@ -12,13 +12,13 @@ describe('CookieParser', function () {
     });
     });
 
 
     it('returns an empty object if no cookies', function () {
     it('returns an empty object if no cookies', function () {
-      const parser = new CookieParser();
+      const parser = new CookiesParser();
       const result = parser.parse({url: '', headers: {}});
       const result = parser.parse({url: '', headers: {}});
       expect(result).to.be.eql({});
       expect(result).to.be.eql({});
     });
     });
 
 
     it('returns an empty object for an empty string', function () {
     it('returns an empty object for an empty string', function () {
-      const parser = new CookieParser();
+      const parser = new CookiesParser();
       const result = parser.parse({url: '', headers: {cookie: ''}});
       const result = parser.parse({url: '', headers: {cookie: ''}});
       expect(result).to.be.eql({});
       expect(result).to.be.eql({});
     });
     });

+ 1 - 1
src/parsers/index.d.ts

@@ -1,4 +1,4 @@
 export * from './body-parser.js';
 export * from './body-parser.js';
 export * from './query-parser.js';
 export * from './query-parser.js';
-export * from './cookie-parser.js';
+export * from './cookies-parser.js';
 export * from './request-parser.js';
 export * from './request-parser.js';

+ 1 - 1
src/parsers/index.js

@@ -1,4 +1,4 @@
 export * from './body-parser.js';
 export * from './body-parser.js';
 export * from './query-parser.js';
 export * from './query-parser.js';
-export * from './cookie-parser.js';
+export * from './cookies-parser.js';
 export * from './request-parser.js';
 export * from './request-parser.js';

+ 4 - 3
src/parsers/query-parser.js

@@ -13,16 +13,17 @@ export class QueryParser extends DebuggableService {
    * @returns {object}
    * @returns {object}
    */
    */
   parse(req) {
   parse(req) {
+    const debug = this.getDebuggerFor(this.parse);
     const queryStr = req.url.replace(/^[^?]*\??/, '');
     const queryStr = req.url.replace(/^[^?]*\??/, '');
     const query = queryStr ? querystring.parse(queryStr) : {};
     const query = queryStr ? querystring.parse(queryStr) : {};
     const queryKeys = Object.keys(query);
     const queryKeys = Object.keys(query);
     if (queryKeys.length) {
     if (queryKeys.length) {
       queryKeys.forEach(key => {
       queryKeys.forEach(key => {
-        this.debug('The query %v has the value %v.', key, query[key]);
+        debug('The query parameter %v had the value %v.', key, query[key]);
       });
       });
     } else {
     } else {
-      this.debug(
-        'The request %s %v has no query.',
+      debug(
+        'The request %s %v had no query parameters.',
         req.method,
         req.method,
         getRequestPathname(req),
         getRequestPathname(req),
       );
       );

+ 2 - 2
src/parsers/request-parser.d.ts

@@ -1,7 +1,7 @@
 import {IncomingMessage} from 'http';
 import {IncomingMessage} from 'http';
 import {ValueOrPromise} from '../types.js';
 import {ValueOrPromise} from '../types.js';
 import {ParsedQuery} from './query-parser.js';
 import {ParsedQuery} from './query-parser.js';
-import {ParsedCookie} from '../utils/index.js';
+import {ParsedCookies} from '../utils/index.js';
 import {DebuggableService} from '../debuggable-service.js';
 import {DebuggableService} from '../debuggable-service.js';
 
 
 /**
 /**
@@ -16,7 +16,7 @@ export type ParsedHeaders = {
  */
  */
 type ParsedRequestData = {
 type ParsedRequestData = {
   query: ParsedQuery;
   query: ParsedQuery;
-  cookie: ParsedCookie;
+  cookies: ParsedCookies;
   body: unknown;
   body: unknown;
   headers: ParsedHeaders;
   headers: ParsedHeaders;
 };
 };

+ 7 - 7
src/parsers/request-parser.js

@@ -3,7 +3,7 @@ import {Errorf} from '@e22m4u/js-format';
 import {isPromise} from '../utils/index.js';
 import {isPromise} from '../utils/index.js';
 import {BodyParser} from './body-parser.js';
 import {BodyParser} from './body-parser.js';
 import {QueryParser} from './query-parser.js';
 import {QueryParser} from './query-parser.js';
-import {CookieParser} from './cookie-parser.js';
+import {CookiesParser} from './cookies-parser.js';
 import {DebuggableService} from '../debuggable-service.js';
 import {DebuggableService} from '../debuggable-service.js';
 
 
 /**
 /**
@@ -20,7 +20,7 @@ export class RequestParser extends DebuggableService {
     if (!(req instanceof IncomingMessage))
     if (!(req instanceof IncomingMessage))
       throw new Errorf(
       throw new Errorf(
         'The first argument of RequestParser.parse should be ' +
         'The first argument of RequestParser.parse should be ' +
-          'an instance of IncomingMessage, but %v given.',
+          'an instance of IncomingMessage, but %v was given.',
         req,
         req,
       );
       );
     const data = {};
     const data = {};
@@ -39,11 +39,11 @@ export class RequestParser extends DebuggableService {
     // данные заголовка "cookie" с проверкой
     // данные заголовка "cookie" с проверкой
     // значения на Promise, и разрываем
     // значения на Promise, и разрываем
     // "eventLoop" при необходимости
     // "eventLoop" при необходимости
-    const parsedCookie = this.getService(CookieParser).parse(req);
-    if (isPromise(parsedCookie)) {
-      promises.push(parsedCookie.then(v => (data.cookie = v)));
+    const parsedCookies = this.getService(CookiesParser).parse(req);
+    if (isPromise(parsedCookies)) {
+      promises.push(parsedCookies.then(v => (data.cookies = v)));
     } else {
     } else {
-      data.cookie = parsedCookie;
+      data.cookies = parsedCookies;
     }
     }
     // аналогично предыдущей операции, разбираем
     // аналогично предыдущей операции, разбираем
     // тело запроса с проверкой результата
     // тело запроса с проверкой результата
@@ -56,7 +56,7 @@ export class RequestParser extends DebuggableService {
     }
     }
     // что бы предотвратить модификацию
     // что бы предотвратить модификацию
     // заголовков, возвращаем их копию
     // заголовков, возвращаем их копию
-    data.headers = JSON.parse(JSON.stringify(req.headers));
+    data.headers = Object.assign({}, req.headers);
     // если имеются асинхронные операции, то результат
     // если имеются асинхронные операции, то результат
     // будет обернут в Promise, в противном случае
     // будет обернут в Promise, в противном случае
     // данные возвращаются сразу
     // данные возвращаются сразу

+ 12 - 12
src/parsers/request-parser.spec.js

@@ -12,7 +12,7 @@ describe('RequestParser', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The first argument of RequestParser.parse should be ' +
           'The first argument of RequestParser.parse should be ' +
-            'an instance of IncomingMessage, but %s given.',
+            'an instance of IncomingMessage, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -29,13 +29,13 @@ describe('RequestParser', function () {
       throwable(createRequestMock())();
       throwable(createRequestMock())();
     });
     });
 
 
-    it('returns the result object if no request body to parse', function () {
+    it('returns the result object if no request body', function () {
       const s = new RequestParser();
       const s = new RequestParser();
       const req = createRequestMock();
       const req = createRequestMock();
       const res = s.parse(req);
       const res = s.parse(req);
       expect(res).to.be.eql({
       expect(res).to.be.eql({
         query: {},
         query: {},
-        cookie: {},
+        cookies: {},
         body: undefined,
         body: undefined,
         headers: {host: 'localhost'},
         headers: {host: 'localhost'},
       });
       });
@@ -54,7 +54,7 @@ describe('RequestParser', function () {
       const res = await promise;
       const res = await promise;
       expect(res).to.be.eql({
       expect(res).to.be.eql({
         query: {},
         query: {},
-        cookie: {},
+        cookies: {},
         body,
         body,
         headers: {
         headers: {
           host: 'localhost',
           host: 'localhost',
@@ -64,25 +64,25 @@ describe('RequestParser', function () {
       });
       });
     });
     });
 
 
-    it('returns the parsed query in the result object', function () {
+    it('returns the result object with the parsed query', function () {
       const s = new RequestParser();
       const s = new RequestParser();
       const req = createRequestMock({path: '/path?p1=foo&p2=bar'});
       const req = createRequestMock({path: '/path?p1=foo&p2=bar'});
       const res = s.parse(req);
       const res = s.parse(req);
       expect(res).to.be.eql({
       expect(res).to.be.eql({
         query: {p1: 'foo', p2: 'bar'},
         query: {p1: 'foo', p2: 'bar'},
-        cookie: {},
+        cookies: {},
         body: undefined,
         body: undefined,
         headers: {host: 'localhost'},
         headers: {host: 'localhost'},
       });
       });
     });
     });
 
 
-    it('returns the parsed cookie in the result object', function () {
+    it('returns the result object with the parsed cookies', function () {
       const s = new RequestParser();
       const s = new RequestParser();
       const req = createRequestMock({headers: {cookie: 'p1=foo; p2=bar;'}});
       const req = createRequestMock({headers: {cookie: 'p1=foo; p2=bar;'}});
       const res = s.parse(req);
       const res = s.parse(req);
       expect(res).to.be.eql({
       expect(res).to.be.eql({
         query: {},
         query: {},
-        cookie: {p1: 'foo', p2: 'bar'},
+        cookies: {p1: 'foo', p2: 'bar'},
         body: undefined,
         body: undefined,
         headers: {
         headers: {
           host: 'localhost',
           host: 'localhost',
@@ -91,7 +91,7 @@ describe('RequestParser', function () {
       });
       });
     });
     });
 
 
-    it('returns the parsed body of the media type "text/plain" in the result object', async function () {
+    it('returns the result object with the parsed body of the media type "text/plain"', async function () {
       const s = new RequestParser();
       const s = new RequestParser();
       const body = 'Lorem Ipsum is simply dummy text.';
       const body = 'Lorem Ipsum is simply dummy text.';
       const req = createRequestMock({
       const req = createRequestMock({
@@ -102,7 +102,7 @@ describe('RequestParser', function () {
       const res = await s.parse(req);
       const res = await s.parse(req);
       expect(res).to.be.eql({
       expect(res).to.be.eql({
         query: {},
         query: {},
-        cookie: {},
+        cookies: {},
         body,
         body,
         headers: {
         headers: {
           host: 'localhost',
           host: 'localhost',
@@ -112,7 +112,7 @@ describe('RequestParser', function () {
       });
       });
     });
     });
 
 
-    it('returns the parsed body of the media type "application/json" in the result object', async function () {
+    it('returns the result object with the parsed body of the media type "application/json"', async function () {
       const s = new RequestParser();
       const s = new RequestParser();
       const body = {foo: 'bar', baz: 'qux'};
       const body = {foo: 'bar', baz: 'qux'};
       const json = JSON.stringify(body);
       const json = JSON.stringify(body);
@@ -124,7 +124,7 @@ describe('RequestParser', function () {
       const res = await s.parse(req);
       const res = await s.parse(req);
       expect(res).to.be.eql({
       expect(res).to.be.eql({
         query: {},
         query: {},
-        cookie: {},
+        cookies: {},
         body,
         body,
         headers: {
         headers: {
           host: 'localhost',
           host: 'localhost',

+ 3 - 3
src/request-context.d.ts

@@ -1,7 +1,7 @@
 import {ServerResponse} from 'http';
 import {ServerResponse} from 'http';
 import {IncomingMessage} from 'http';
 import {IncomingMessage} from 'http';
-import {ParsedCookie} from './utils/index.js';
 import {ParsedQuery} from './parsers/index.js';
 import {ParsedQuery} from './parsers/index.js';
+import {ParsedCookies} from './utils/index.js';
 import {ParsedHeaders} from './parsers/index.js';
 import {ParsedHeaders} from './parsers/index.js';
 import {ServiceContainer} from '@e22m4u/js-service';
 import {ServiceContainer} from '@e22m4u/js-service';
 
 
@@ -47,9 +47,9 @@ export declare class RequestContext {
   headers: ParsedHeaders;
   headers: ParsedHeaders;
 
 
   /**
   /**
-   * Cookie.
+   * Cookies.
    */
    */
-  cookie: ParsedCookie;
+  cookies: ParsedCookies;
 
 
   /**
   /**
    * Body.
    * Body.

+ 5 - 5
src/request-context.js

@@ -52,11 +52,11 @@ export class RequestContext {
   headers = {};
   headers = {};
 
 
   /**
   /**
-   * Parsed cookie.
+   * Parsed cookies.
    *
    *
    * @type {object}
    * @type {object}
    */
    */
-  cookie = {};
+  cookies = {};
 
 
   /**
   /**
    * Parsed body.
    * Parsed body.
@@ -113,7 +113,7 @@ export class RequestContext {
     if (!isServiceContainer(container))
     if (!isServiceContainer(container))
       throw new Errorf(
       throw new Errorf(
         'The parameter "container" of RequestContext.constructor ' +
         'The parameter "container" of RequestContext.constructor ' +
-          'should be an instance of ServiceContainer, but %v given.',
+          'should be an instance of ServiceContainer, but %v was given.',
         container,
         container,
       );
       );
     this.container = container;
     this.container = container;
@@ -125,7 +125,7 @@ export class RequestContext {
     ) {
     ) {
       throw new Errorf(
       throw new Errorf(
         'The parameter "request" of RequestContext.constructor ' +
         'The parameter "request" of RequestContext.constructor ' +
-          'should be an instance of IncomingMessage, but %v given.',
+          'should be an instance of IncomingMessage, but %v was given.',
         request,
         request,
       );
       );
     }
     }
@@ -138,7 +138,7 @@ export class RequestContext {
     ) {
     ) {
       throw new Errorf(
       throw new Errorf(
         'The parameter "response" of RequestContext.constructor ' +
         'The parameter "response" of RequestContext.constructor ' +
-          'should be an instance of ServerResponse, but %v given.',
+          'should be an instance of ServerResponse, but %v was given.',
         response,
         response,
       );
       );
     }
     }

+ 3 - 3
src/request-context.spec.js

@@ -14,7 +14,7 @@ describe('RequestContext', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "container" of RequestContext.constructor ' +
           'The parameter "container" of RequestContext.constructor ' +
-            'should be an instance of ServiceContainer, but %s given.',
+            'should be an instance of ServiceContainer, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -37,7 +37,7 @@ describe('RequestContext', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "request" of RequestContext.constructor ' +
           'The parameter "request" of RequestContext.constructor ' +
-            'should be an instance of IncomingMessage, but %s given.',
+            'should be an instance of IncomingMessage, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -60,7 +60,7 @@ describe('RequestContext', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The parameter "response" of RequestContext.constructor ' +
           'The parameter "response" of RequestContext.constructor ' +
-            'should be an instance of ServerResponse, but %s given.',
+            'should be an instance of ServerResponse, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));

+ 14 - 14
src/route-registry.js

@@ -32,16 +32,17 @@ export class RouteRegistry extends DebuggableService {
    * @returns {Route}
    * @returns {Route}
    */
    */
   defineRoute(routeDef) {
   defineRoute(routeDef) {
+    const debug = this.getDebuggerFor(this.defineRoute);
     if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef))
     if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef))
       throw new Errorf(
       throw new Errorf(
-        'The route definition should be an Object, but %v given.',
+        'The route definition should be an Object, but %v was given.',
         routeDef,
         routeDef,
       );
       );
     const route = new Route(routeDef);
     const route = new Route(routeDef);
     const triePath = `${route.method}/${route.path}`;
     const triePath = `${route.method}/${route.path}`;
     this._trie.add(triePath, route);
     this._trie.add(triePath, route);
-    this.debug(
-      'The route %s %v is registered.',
+    debug(
+      'The route %s %v was registered.',
       route.method.toUpperCase(),
       route.method.toUpperCase(),
       route.path,
       route.path,
     );
     );
@@ -55,9 +56,10 @@ export class RouteRegistry extends DebuggableService {
    * @returns {ResolvedRoute|undefined}
    * @returns {ResolvedRoute|undefined}
    */
    */
   matchRouteByRequest(req) {
   matchRouteByRequest(req) {
+    const debug = this.getDebuggerFor(this.matchRouteByRequest);
     const requestPath = (req.url || '/').replace(/\?.*$/, '');
     const requestPath = (req.url || '/').replace(/\?.*$/, '');
-    this.debug(
-      'Matching %s %v with registered routes.',
+    debug(
+      'Matching routes with the request %s %v.',
       req.method.toUpperCase(),
       req.method.toUpperCase(),
       requestPath,
       requestPath,
     );
     );
@@ -65,28 +67,26 @@ export class RouteRegistry extends DebuggableService {
     const resolved = this._trie.match(triePath);
     const resolved = this._trie.match(triePath);
     if (resolved) {
     if (resolved) {
       const route = resolved.value;
       const route = resolved.value;
-      this.debug(
-        'The request %s %v was matched to the route %s %v.',
-        req.method.toUpperCase(),
-        requestPath,
+      debug(
+        'The route %s %v was matched.',
         route.method.toUpperCase(),
         route.method.toUpperCase(),
         route.path,
         route.path,
       );
       );
       const paramNames = Object.keys(resolved.params);
       const paramNames = Object.keys(resolved.params);
-      if (paramNames) {
+      if (paramNames.length) {
         paramNames.forEach(name => {
         paramNames.forEach(name => {
-          this.debug(
-            'The path parameter %v has the value %v.',
+          debug(
+            'The path parameter %v had the value %v.',
             name,
             name,
             resolved.params[name],
             resolved.params[name],
           );
           );
         });
         });
       } else {
       } else {
-        this.debug('No path parameters found.');
+        debug('No path parameters found.');
       }
       }
       return {route, params: resolved.params};
       return {route, params: resolved.params};
     }
     }
-    this.debug(
+    debug(
       'No matched route for the request %s %v.',
       'No matched route for the request %s %v.',
       req.method.toUpperCase(),
       req.method.toUpperCase(),
       requestPath,
       requestPath,

+ 4 - 1
src/route-registry.spec.js

@@ -11,7 +11,10 @@ describe('RouteRegistry', function () {
       const s = new RouteRegistry();
       const s = new RouteRegistry();
       const throwable = v => () => s.defineRoute(v);
       const throwable = v => () => s.defineRoute(v);
       const error = v =>
       const error = v =>
-        format('The route definition should be an Object, but %s given.', v);
+        format(
+          'The route definition should be an Object, but %s was given.',
+          v,
+        );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));

+ 12 - 7
src/route.d.ts

@@ -5,13 +5,18 @@ import {RequestContext} from './request-context.js';
 /**
 /**
  * Http method.
  * Http method.
  */
  */
-export enum HttpMethod {
-  GET = 'get',
-  POST = 'post',
-  PUT = 'put',
-  PATCH = 'patch',
-  DELETE = 'delete',
-}
+export declare const HttpMethod: {
+  GET: 'GET';
+  POST: 'POST';
+  PUT: 'PUT';
+  PATCH: 'PATCH';
+  DELETE: 'DELETE';
+};
+
+/**
+ * Type of HttpMethod.
+ */
+export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod];
 
 
 /**
 /**
  * Route handler.
  * Route handler.

+ 16 - 14
src/route.js

@@ -1,8 +1,9 @@
 import {Errorf} from '@e22m4u/js-format';
 import {Errorf} from '@e22m4u/js-format';
 import {HookType} from './hooks/index.js';
 import {HookType} from './hooks/index.js';
+import {Debuggable} from '@e22m4u/js-debug';
 import {HookRegistry} from './hooks/index.js';
 import {HookRegistry} from './hooks/index.js';
-import {createDebugger} from './utils/index.js';
 import {getRequestPathname} from './utils/index.js';
 import {getRequestPathname} from './utils/index.js';
+import {MODULE_DEBUG_NAMESPACE} from './debuggable-service.js';
 
 
 /**
 /**
  * @typedef {import('./request-context.js').RequestContext} RequestContext
  * @typedef {import('./request-context.js').RequestContext} RequestContext
@@ -37,17 +38,10 @@ export const HttpMethod = {
   DELETE: 'DELETE',
   DELETE: 'DELETE',
 };
 };
 
 
-/**
- * Debugger.
- *
- * @type {Function}
- */
-const debug = createDebugger('route');
-
 /**
 /**
  * Route.
  * Route.
  */
  */
-export class Route {
+export class Route extends Debuggable {
   /**
   /**
    * Method.
    * Method.
    *
    *
@@ -122,29 +116,35 @@ export class Route {
    * @param {RouteDefinition} routeDef
    * @param {RouteDefinition} routeDef
    */
    */
   constructor(routeDef) {
   constructor(routeDef) {
+    super({
+      namespace: MODULE_DEBUG_NAMESPACE,
+      noEnvironmentNamespace: true,
+      noInstantiationMessage: true,
+    });
     if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef))
     if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef))
       throw new Errorf(
       throw new Errorf(
-        'The first parameter of Route.controller ' +
-          'should be an Object, but %v given.',
+        'The first parameter of Route.constructor ' +
+          'should be an Object, but %v was given.',
         routeDef,
         routeDef,
       );
       );
     if (!routeDef.method || typeof routeDef.method !== 'string')
     if (!routeDef.method || typeof routeDef.method !== 'string')
       throw new Errorf(
       throw new Errorf(
         'The option "method" of the Route should be ' +
         'The option "method" of the Route should be ' +
-          'a non-empty String, but %v given.',
+          'a non-empty String, but %v was given.',
         routeDef.method,
         routeDef.method,
       );
       );
     this._method = routeDef.method.toUpperCase();
     this._method = routeDef.method.toUpperCase();
     if (typeof routeDef.path !== 'string')
     if (typeof routeDef.path !== 'string')
       throw new Errorf(
       throw new Errorf(
-        'The option "path" of the Route should be ' + 'a String, but %v given.',
+        'The option "path" of the Route should be ' +
+          'a String, but %v was given.',
         routeDef.path,
         routeDef.path,
       );
       );
     this._path = routeDef.path;
     this._path = routeDef.path;
     if (typeof routeDef.handler !== 'function')
     if (typeof routeDef.handler !== 'function')
       throw new Errorf(
       throw new Errorf(
         'The option "handler" of the Route should be ' +
         'The option "handler" of the Route should be ' +
-          'a Function, but %v given.',
+          'a Function, but %v was given.',
         routeDef.handler,
         routeDef.handler,
       );
       );
     this._handler = routeDef.handler;
     this._handler = routeDef.handler;
@@ -164,6 +164,7 @@ export class Route {
         this._hookRegistry.addHook(HookType.POST_HANDLER, hook);
         this._hookRegistry.addHook(HookType.POST_HANDLER, hook);
       });
       });
     }
     }
+    this.ctorDebug('A new route %s %v was created.', this._method, this._path);
   }
   }
 
 
   /**
   /**
@@ -173,6 +174,7 @@ export class Route {
    * @returns {*}
    * @returns {*}
    */
    */
   handle(context) {
   handle(context) {
+    const debug = this.getDebuggerFor(this.handle);
     const requestPath = getRequestPathname(context.req);
     const requestPath = getRequestPathname(context.req);
     debug(
     debug(
       'Invoking the Route handler for the request %s %v.',
       'Invoking the Route handler for the request %s %v.',

+ 13 - 7
src/route.spec.js

@@ -14,8 +14,8 @@ describe('Route', function () {
       const throwable = v => () => new Route(v);
       const throwable = v => () => new Route(v);
       const error = v =>
       const error = v =>
         format(
         format(
-          'The first parameter of Route.controller ' +
-            'should be an Object, but %s given.',
+          'The first parameter of Route.constructor ' +
+            'should be an Object, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -45,7 +45,7 @@ describe('Route', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The option "method" of the Route should be ' +
           'The option "method" of the Route should be ' +
-            'a non-empty String, but %s given.',
+            'a non-empty String, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('')).to.throw(error('""'));
       expect(throwable('')).to.throw(error('""'));
@@ -71,7 +71,7 @@ describe('Route', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The option "path" of the Route should be ' +
           'The option "path" of the Route should be ' +
-            'a String, but %s given.',
+            'a String, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(10)).to.throw(error('10'));
@@ -97,7 +97,7 @@ describe('Route', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The option "handler" of the Route should be ' +
           'The option "handler" of the Route should be ' +
-            'a Function, but %s given.',
+            'a Function, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));
@@ -122,7 +122,10 @@ describe('Route', function () {
           handler: () => undefined,
           handler: () => undefined,
         });
         });
       const error = v =>
       const error = v =>
-        format('The hook "preHandler" should be a Function, but %s given.', v);
+        format(
+          'The hook "preHandler" should be a Function, but %s was given.',
+          v,
+        );
       expect(throwable1('str')).to.throw(error('"str"'));
       expect(throwable1('str')).to.throw(error('"str"'));
       expect(throwable1('')).to.throw(error('""'));
       expect(throwable1('')).to.throw(error('""'));
       expect(throwable1(10)).to.throw(error('10'));
       expect(throwable1(10)).to.throw(error('10'));
@@ -163,7 +166,10 @@ describe('Route', function () {
           postHandler: v,
           postHandler: v,
         });
         });
       const error = v =>
       const error = v =>
-        format('The hook "postHandler" should be a Function, but %s given.', v);
+        format(
+          'The hook "postHandler" should be a Function, but %s was given.',
+          v,
+        );
       expect(throwable1('str')).to.throw(error('"str"'));
       expect(throwable1('str')).to.throw(error('"str"'));
       expect(throwable1('')).to.throw(error('""'));
       expect(throwable1('')).to.throw(error('""'));
       expect(throwable1(10)).to.throw(error('10'));
       expect(throwable1(10)).to.throw(error('10'));

+ 1 - 1
src/router-options.js

@@ -32,7 +32,7 @@ export class RouterOptions extends DebuggableService {
     if (typeof input !== 'number' || input < 0)
     if (typeof input !== 'number' || input < 0)
       throw new Errorf(
       throw new Errorf(
         'The option "requestBodyBytesLimit" must be ' +
         'The option "requestBodyBytesLimit" must be ' +
-          'a positive Number or 0, but %v given.',
+          'a positive Number or 0, but %v was given.',
         input,
         input,
       );
       );
     this._requestBodyBytesLimit = input;
     this._requestBodyBytesLimit = input;

+ 1 - 1
src/router-options.spec.js

@@ -25,7 +25,7 @@ describe('RouterOptions', function () {
       const error = v =>
       const error = v =>
         format(
         format(
           'The option "requestBodyBytesLimit" must be ' +
           'The option "requestBodyBytesLimit" must be ' +
-            'a positive Number or 0, but %s given.',
+            'a positive Number or 0, but %s was given.',
           v,
           v,
         );
         );
       expect(throwable('str')).to.throw(error('"str"'));
       expect(throwable('str')).to.throw(error('"str"'));

+ 6 - 5
src/senders/data-sender.js

@@ -14,14 +14,15 @@ export class DataSender extends DebuggableService {
    * @returns {undefined}
    * @returns {undefined}
    */
    */
   send(res, data) {
   send(res, data) {
+    const debug = this.getDebuggerFor(this.send);
     // если ответ контроллера является объектом
     // если ответ контроллера является объектом
     // ServerResponse, или имеются отправленные
     // ServerResponse, или имеются отправленные
     // заголовки, то считаем, что контроллер
     // заголовки, то считаем, что контроллер
     // уже отправил ответ самостоятельно
     // уже отправил ответ самостоятельно
     if (data === res || res.headersSent) {
     if (data === res || res.headersSent) {
-      this.debug(
+      debug(
         'Response sending was skipped because ' +
         'Response sending was skipped because ' +
-          'its headers where sent already .',
+          'its headers where sent already.',
       );
       );
       return;
       return;
     }
     }
@@ -30,7 +31,7 @@ export class DataSender extends DebuggableService {
     if (data == null) {
     if (data == null) {
       res.statusCode = 204;
       res.statusCode = 204;
       res.end();
       res.end();
-      this.debug('The empty response was sent.');
+      debug('The empty response was sent.');
       return;
       return;
     }
     }
     // если ответ контроллера является стримом,
     // если ответ контроллера является стримом,
@@ -38,7 +39,7 @@ export class DataSender extends DebuggableService {
     if (isReadableStream(data)) {
     if (isReadableStream(data)) {
       res.setHeader('Content-Type', 'application/octet-stream');
       res.setHeader('Content-Type', 'application/octet-stream');
       data.pipe(res);
       data.pipe(res);
-      this.debug('The stream response was sent.');
+      debug('The stream response was sent.');
       return;
       return;
     }
     }
     // подготовка данных перед отправкой, и установка
     // подготовка данных перед отправкой, и установка
@@ -67,6 +68,6 @@ export class DataSender extends DebuggableService {
     }
     }
     // отправка подготовленных данных
     // отправка подготовленных данных
     res.end(data);
     res.end(data);
-    this.debug(debugMsg);
+    debug(debugMsg);
   }
   }
 }
 }

+ 6 - 4
src/senders/error-sender.js

@@ -23,6 +23,7 @@ export class ErrorSender extends DebuggableService {
    * @returns {undefined}
    * @returns {undefined}
    */
    */
   send(req, res, error) {
   send(req, res, error) {
+    const debug = this.getDebuggerFor(this.send);
     let safeError = {};
     let safeError = {};
     if (error) {
     if (error) {
       if (typeof error === 'object') {
       if (typeof error === 'object') {
@@ -62,8 +63,8 @@ export class ErrorSender extends DebuggableService {
     res.statusCode = statusCode;
     res.statusCode = statusCode;
     res.setHeader('content-type', 'application/json; charset=utf-8');
     res.setHeader('content-type', 'application/json; charset=utf-8');
     res.end(JSON.stringify(body, null, 2), 'utf-8');
     res.end(JSON.stringify(body, null, 2), 'utf-8');
-    this.debug(
-      'The %s error is sent for the request %s %v.',
+    debug(
+      'The %s error was sent for the request %s %v.',
       statusCode,
       statusCode,
       req.method,
       req.method,
       getRequestPathname(req),
       getRequestPathname(req),
@@ -78,11 +79,12 @@ export class ErrorSender extends DebuggableService {
    * @returns {undefined}
    * @returns {undefined}
    */
    */
   send404(req, res) {
   send404(req, res) {
+    const debug = this.getDebuggerFor(this.send404);
     res.statusCode = 404;
     res.statusCode = 404;
     res.setHeader('content-type', 'text/plain; charset=utf-8');
     res.setHeader('content-type', 'text/plain; charset=utf-8');
     res.end('404 Not Found', 'utf-8');
     res.end('404 Not Found', 'utf-8');
-    this.debug(
-      'The 404 error is sent for the request %s %v.',
+    debug(
+      'The 404 error was sent for the request %s %v.',
       req.method,
       req.method,
       getRequestPathname(req),
       getRequestPathname(req),
     );
     );

+ 24 - 14
src/trie-router.js

@@ -1,5 +1,5 @@
 import {HookType} from './hooks/index.js';
 import {HookType} from './hooks/index.js';
-import {isPromise} from './utils/index.js';
+import {isPromise, isResponseSent} from './utils/index.js';
 import {HookInvoker} from './hooks/index.js';
 import {HookInvoker} from './hooks/index.js';
 import {DataSender} from './senders/index.js';
 import {DataSender} from './senders/index.js';
 import {HookRegistry} from './hooks/index.js';
 import {HookRegistry} from './hooks/index.js';
@@ -31,11 +31,11 @@ export class TrieRouter extends DebuggableService {
    * ```
    * ```
    * const router = new TrieRouter();
    * const router = new TrieRouter();
    * router.defineRoute({
    * router.defineRoute({
-   *   method: HttpMethod.POST,       // Request method.
+   *   method: HttpMethod.POST,        // Request method.
    *   path: '/users/:id',             // The path template may have parameters.
    *   path: '/users/:id',             // The path template may have parameters.
-   *   preHandler(ctx) { ... },        // The "preHandler" is executed before a route handler.
+   *   preHandler(ctx) { ... },        // The "preHandler" executes before a route handler.
    *   handler(ctx) { ... },           // Request handler function.
    *   handler(ctx) { ... },           // Request handler function.
-   *   postHandler(ctx, data) { ... }, // The "postHandler" is executed after a route handler.
+   *   postHandler(ctx, data) { ... }, // The "postHandler" executes after a route handler.
    * });
    * });
    * ```
    * ```
    *
    *
@@ -75,11 +75,16 @@ export class TrieRouter extends DebuggableService {
    * @private
    * @private
    */
    */
   async _handleRequest(req, res) {
   async _handleRequest(req, res) {
+    const debug = this.getDebuggerFor(this._handleRequest);
     const requestPath = (req.url || '/').replace(/\?.*$/, '');
     const requestPath = (req.url || '/').replace(/\?.*$/, '');
-    this.debug('Preparing to handle %s %v.', req.method, requestPath);
+    debug(
+      'Preparing to handle an incoming request %s %v.',
+      req.method,
+      requestPath,
+    );
     const resolved = this.getService(RouteRegistry).matchRouteByRequest(req);
     const resolved = this.getService(RouteRegistry).matchRouteByRequest(req);
     if (!resolved) {
     if (!resolved) {
-      this.debug('No route for the request %s %v.', req.method, requestPath);
+      debug('No route for the request %s %v.', req.method, requestPath);
       this.getService(ErrorSender).send404(req, res);
       this.getService(ErrorSender).send404(req, res);
     } else {
     } else {
       const {route, params} = resolved;
       const {route, params} = resolved;
@@ -127,15 +132,15 @@ export class TrieRouter extends DebuggableService {
           context,
           context,
         );
         );
         if (isPromise(data)) data = await data;
         if (isPromise(data)) data = await data;
-        // если ответ не определен хуками "preHandler",
-        // то вызывается обработчик роута, результат
-        // которого передается в хуки "postHandler"
-        if (data == null) {
+        // если ответ не бы отправлен внутри "preHandler" хуков,
+        // и сами "preHandler" хуки не вернули значения, то вызывается
+        // основной обработчик маршрута, результат которого передается
+        // в хуки "postHandler"
+        if (!isResponseSent(res) && data == null) {
           data = route.handle(context);
           data = route.handle(context);
           if (isPromise(data)) data = await data;
           if (isPromise(data)) data = await data;
-          // вызываются хуки "postHandler", результат
-          // которых также может быть использован
-          // в качестве ответа
+          // вызываются хуки "postHandler", результат которых
+          // также может быть использован в качестве ответа
           let postHandlerData = hookInvoker.invokeAndContinueUntilValueReceived(
           let postHandlerData = hookInvoker.invokeAndContinueUntilValueReceived(
             route,
             route,
             HookType.POST_HANDLER,
             HookType.POST_HANDLER,
@@ -151,7 +156,12 @@ export class TrieRouter extends DebuggableService {
         this.getService(ErrorSender).send(req, res, error);
         this.getService(ErrorSender).send(req, res, error);
         return;
         return;
       }
       }
-      this.getService(DataSender).send(res, data);
+      // если ответ не был отправлен во время выполнения
+      // хуков и основного обработчика запроса,
+      // то результат передается в DataSender
+      if (!isResponseSent(res)) {
+        this.getService(DataSender).send(res, data);
+      }
     }
     }
   }
   }
 
 

+ 35 - 5
src/trie-router.spec.js

@@ -75,13 +75,13 @@ describe('TrieRouter', function () {
       router.requestListener(req, res);
       router.requestListener(req, res);
     });
     });
 
 
-    it('passes parsed cookie to the request context', function (done) {
+    it('passes parsed cookies to the request context', function (done) {
       const router = new TrieRouter();
       const router = new TrieRouter();
       router.defineRoute({
       router.defineRoute({
         method: HttpMethod.GET,
         method: HttpMethod.GET,
         path: '/',
         path: '/',
-        handler: ({cookie}) => {
-          expect(cookie).to.be.eql({p1: 'foo', p2: 'bar'});
+        handler: ({cookies}) => {
+          expect(cookies).to.be.eql({p1: 'foo', p2: 'bar'});
           done();
           done();
         },
         },
       });
       });
@@ -483,16 +483,46 @@ describe('TrieRouter', function () {
       const req = createRequestMock({
       const req = createRequestMock({
         method: HttpMethod.POST,
         method: HttpMethod.POST,
         headers: {'content-type': 'application/json'},
         headers: {'content-type': 'application/json'},
-        body: 'invalid json',
+        body: 'invalid',
       });
       });
       const res = createResponseMock();
       const res = createResponseMock();
       router.requestListener(req, res);
       router.requestListener(req, res);
       const body = await res.getBody();
       const body = await res.getBody();
       expect(res.statusCode).to.be.eq(400);
       expect(res.statusCode).to.be.eq(400);
       expect(JSON.parse(body)).to.be.eql({
       expect(JSON.parse(body)).to.be.eql({
-        error: {message: 'Unable to parse request body.'},
+        error: {
+          message: `Unexpected token 'i', "invalid" is not valid JSON`,
+        },
       });
       });
     });
     });
+
+    it('should not invoke the main handler if a preHandler sends the response asynchronously', async function () {
+      let handlerCalled = false;
+      const router = new TrieRouter();
+      router.defineRoute({
+        method: 'GET',
+        path: '/',
+        preHandler(ctx) {
+          return new Promise(resolve => {
+            setTimeout(() => {
+              ctx.res.setHeader('Content-Type', 'text/plain');
+              ctx.res.end('Response from preHandler');
+              resolve(undefined);
+            }, 10);
+          });
+        },
+        handler() {
+          handlerCalled = true;
+          return 'Response from main handler';
+        },
+      });
+      const req = createRequestMock({method: 'GET', path: '/'});
+      const res = createResponseMock();
+      await router._handleRequest(req, res);
+      const responseBody = await res.getBody();
+      expect(responseBody).to.equal('Response from preHandler');
+      expect(handlerCalled).to.be.false;
+    });
   });
   });
 
 
   describe('addHook', function () {
   describe('addHook', function () {

+ 0 - 6
src/utils/create-cookie-string.d.ts

@@ -1,6 +0,0 @@
-/**
- * Create cookie string.
- *
- * @param data
- */
-export declare function createCookieString(data: object): string;

+ 6 - 0
src/utils/create-cookies-string.d.ts

@@ -0,0 +1,6 @@
+/**
+ * Create cookies string.
+ *
+ * @param data
+ */
+export declare function createCookiesString(data: object): string;

+ 4 - 4
src/utils/create-cookie-string.js → src/utils/create-cookies-string.js

@@ -1,16 +1,16 @@
 import {Errorf} from '@e22m4u/js-format';
 import {Errorf} from '@e22m4u/js-format';
 
 
 /**
 /**
- * Create cookie string.
+ * Create cookies string.
  *
  *
  * @param {object} data
  * @param {object} data
  * @returns {string}
  * @returns {string}
  */
  */
-export function createCookieString(data) {
+export function createCookiesString(data) {
   if (!data || typeof data !== 'object' || Array.isArray(data))
   if (!data || typeof data !== 'object' || Array.isArray(data))
     throw new Errorf(
     throw new Errorf(
-      'The first parameter of "createCookieString" should be ' +
-        'an Object, but %v given.',
+      'The first parameter of "createCookiesString" should be ' +
+        'an Object, but %v was given.',
       data,
       data,
     );
     );
   let cookies = '';
   let cookies = '';

+ 7 - 7
src/utils/create-cookie-string.spec.js → src/utils/create-cookies-string.spec.js

@@ -1,14 +1,14 @@
 import {expect} from '../chai.js';
 import {expect} from '../chai.js';
 import {format} from '@e22m4u/js-format';
 import {format} from '@e22m4u/js-format';
-import {createCookieString} from './create-cookie-string.js';
+import {createCookiesString} from './create-cookies-string.js';
 
 
-describe('createCookieString', function () {
+describe('createCookiesString', function () {
   it('requires the first argument to be an object', function () {
   it('requires the first argument to be an object', function () {
-    const throwable = v => () => createCookieString(v);
+    const throwable = v => () => createCookiesString(v);
     const error = v =>
     const error = v =>
       format(
       format(
-        'The first parameter of "createCookieString" should be ' +
-          'an Object, but %s given.',
+        'The first parameter of "createCookiesString" should be ' +
+          'an Object, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -25,12 +25,12 @@ describe('createCookieString', function () {
   });
   });
 
 
   it('returns an empty string if no keys', function () {
   it('returns an empty string if no keys', function () {
-    expect(createCookieString({})).to.be.eq('');
+    expect(createCookiesString({})).to.be.eq('');
   });
   });
 
 
   it('returns a cookies string from a given object', function () {
   it('returns a cookies string from a given object', function () {
     const data = {foo: 'bar', baz: 'quz'};
     const data = {foo: 'bar', baz: 'quz'};
-    const result = createCookieString(data);
+    const result = createCookiesString(data);
     expect(result).to.be.eq('foo=bar; baz=quz;');
     expect(result).to.be.eq('foo=bar; baz=quz;');
   });
   });
 });
 });

+ 1 - 1
src/utils/create-debugger.js

@@ -11,7 +11,7 @@ export function createDebugger(name) {
   if (typeof name !== 'string')
   if (typeof name !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The first argument of "createDebugger" should be ' +
       'The first argument of "createDebugger" should be ' +
-        'a String, but %v given.',
+        'a String, but %v was given.',
       name,
       name,
     );
     );
   const debug = DebugFactory(`jsTrieRouter:${name}`);
   const debug = DebugFactory(`jsTrieRouter:${name}`);

+ 1 - 1
src/utils/create-debugger.spec.js

@@ -8,7 +8,7 @@ describe('createDebugger', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first argument of "createDebugger" should be ' +
         'The first argument of "createDebugger" should be ' +
-          'a String, but %s given.',
+          'a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));

+ 2 - 2
src/utils/create-error.js

@@ -13,13 +13,13 @@ export function createError(errorCtor, message, ...args) {
   if (typeof errorCtor !== 'function')
   if (typeof errorCtor !== 'function')
     throw new Errorf(
     throw new Errorf(
       'The first argument of "createError" should be ' +
       'The first argument of "createError" should be ' +
-        'a constructor, but %v given.',
+        'a constructor, but %v was given.',
       errorCtor,
       errorCtor,
     );
     );
   if (message != null && typeof message !== 'string')
   if (message != null && typeof message !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The second argument of "createError" should be ' +
       'The second argument of "createError" should be ' +
-        'a String, but %v given.',
+        'a String, but %v was given.',
       message,
       message,
     );
     );
   if (message == null) return new errorCtor();
   if (message == null) return new errorCtor();

+ 2 - 2
src/utils/create-error.spec.js

@@ -8,7 +8,7 @@ describe('createError', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first argument of "createError" should be ' +
         'The first argument of "createError" should be ' +
-          'a constructor, but %s given.',
+          'a constructor, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -29,7 +29,7 @@ describe('createError', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The second argument of "createError" should be ' +
         'The second argument of "createError" should be ' +
-          'a String, but %s given.',
+          'a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));

+ 1 - 1
src/utils/create-request-mock.d.ts

@@ -10,7 +10,7 @@ type RequestPatch = {
   secure?: boolean;
   secure?: boolean;
   path?: string;
   path?: string;
   query?: object;
   query?: object;
-  cookie?: object;
+  cookies?: object;
   headers?: object;
   headers?: object;
   body?: string;
   body?: string;
   stream?: Readable;
   stream?: Readable;

+ 47 - 43
src/utils/create-request-mock.js

@@ -4,8 +4,8 @@ import {IncomingMessage} from 'http';
 import queryString from 'querystring';
 import queryString from 'querystring';
 import {Errorf} from '@e22m4u/js-format';
 import {Errorf} from '@e22m4u/js-format';
 import {isReadableStream} from './is-readable-stream.js';
 import {isReadableStream} from './is-readable-stream.js';
-import {createCookieString} from './create-cookie-string.js';
-import {BUFFER_ENCODING_LIST} from './fetch-request-body.js';
+import {createCookiesString} from './create-cookies-string.js';
+import {CHARACTER_ENCODING_LIST} from './fetch-request-body.js';
 
 
 /**
 /**
  * @typedef {{
  * @typedef {{
@@ -14,7 +14,7 @@ import {BUFFER_ENCODING_LIST} from './fetch-request-body.js';
  *   secure?: boolean;
  *   secure?: boolean;
  *   path?: string;
  *   path?: string;
  *   query?: object;
  *   query?: object;
- *   cookie?: object;
+ *   cookies?: object;
  *   headers?: object;
  *   headers?: object;
  *   body?: string;
  *   body?: string;
  *   stream?: import('stream').Readable;
  *   stream?: import('stream').Readable;
@@ -32,7 +32,7 @@ export function createRequestMock(patch) {
   if ((patch != null && typeof patch !== 'object') || Array.isArray(patch)) {
   if ((patch != null && typeof patch !== 'object') || Array.isArray(patch)) {
     throw new Errorf(
     throw new Errorf(
       'The first parameter of "createRequestMock" ' +
       'The first parameter of "createRequestMock" ' +
-        'should be an Object, but %v given.',
+        'should be an Object, but %v was given.',
       patch,
       patch,
     );
     );
   }
   }
@@ -40,25 +40,25 @@ export function createRequestMock(patch) {
   if (patch.host != null && typeof patch.host !== 'string')
   if (patch.host != null && typeof patch.host !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "host" of "createRequestMock" ' +
       'The parameter "host" of "createRequestMock" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       patch.host,
       patch.host,
     );
     );
   if (patch.method != null && typeof patch.method !== 'string')
   if (patch.method != null && typeof patch.method !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "method" of "createRequestMock" ' +
       'The parameter "method" of "createRequestMock" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       patch.method,
       patch.method,
     );
     );
   if (patch.secure != null && typeof patch.secure !== 'boolean')
   if (patch.secure != null && typeof patch.secure !== 'boolean')
     throw new Errorf(
     throw new Errorf(
       'The parameter "secure" of "createRequestMock" ' +
       'The parameter "secure" of "createRequestMock" ' +
-        'should be a Boolean, but %v given.',
+        'should be a Boolean, but %v was given.',
       patch.secure,
       patch.secure,
     );
     );
   if (patch.path != null && typeof patch.path !== 'string')
   if (patch.path != null && typeof patch.path !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "path" of "createRequestMock" ' +
       'The parameter "path" of "createRequestMock" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       patch.path,
       patch.path,
     );
     );
   if (
   if (
@@ -69,20 +69,20 @@ export function createRequestMock(patch) {
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
       'The parameter "query" of "createRequestMock" ' +
       'The parameter "query" of "createRequestMock" ' +
-        'should be a String or Object, but %v given.',
+        'should be a String or Object, but %v was given.',
       patch.query,
       patch.query,
     );
     );
   }
   }
   if (
   if (
-    (patch.cookie != null &&
-      typeof patch.cookie !== 'string' &&
-      typeof patch.cookie !== 'object') ||
-    Array.isArray(patch.cookie)
+    (patch.cookies != null &&
+      typeof patch.cookies !== 'string' &&
+      typeof patch.cookies !== 'object') ||
+    Array.isArray(patch.cookies)
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
-      'The parameter "cookie" of "createRequestMock" ' +
-        'should be a String or Object, but %v given.',
-      patch.cookie,
+      'The parameter "cookies" of "createRequestMock" ' +
+        'should be a String or Object, but %v was given.',
+      patch.cookies,
     );
     );
   }
   }
   if (
   if (
@@ -91,25 +91,28 @@ export function createRequestMock(patch) {
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
       'The parameter "headers" of "createRequestMock" ' +
       'The parameter "headers" of "createRequestMock" ' +
-        'should be an Object, but %v given.',
+        'should be an Object, but %v was given.',
       patch.headers,
       patch.headers,
     );
     );
   }
   }
   if (patch.stream != null && !isReadableStream(patch.stream))
   if (patch.stream != null && !isReadableStream(patch.stream))
     throw new Errorf(
     throw new Errorf(
       'The parameter "stream" of "createRequestMock" ' +
       'The parameter "stream" of "createRequestMock" ' +
-        'should be a Stream, but %v given.',
+        'should be a Stream, but %v was given.',
       patch.stream,
       patch.stream,
     );
     );
   if (patch.encoding != null) {
   if (patch.encoding != null) {
     if (typeof patch.encoding !== 'string')
     if (typeof patch.encoding !== 'string')
       throw new Errorf(
       throw new Errorf(
         'The parameter "encoding" of "createRequestMock" ' +
         'The parameter "encoding" of "createRequestMock" ' +
-          'should be a String, but %v given.',
+          'should be a String, but %v was given.',
+        patch.encoding,
+      );
+    if (!CHARACTER_ENCODING_LIST.includes(patch.encoding))
+      throw new Errorf(
+        'Character encoding %v is not supported.',
         patch.encoding,
         patch.encoding,
       );
       );
-    if (!BUFFER_ENCODING_LIST.includes(patch.encoding))
-      throw new Errorf('Buffer encoding %v is not supported.', patch.encoding);
   }
   }
   // если передан поток, выполняется
   // если передан поток, выполняется
   // проверка на несовместимые опции
   // проверка на несовместимые опции
@@ -141,7 +144,7 @@ export function createRequestMock(patch) {
     patch.host,
     patch.host,
     patch.secure,
     patch.secure,
     patch.body,
     patch.body,
-    patch.cookie,
+    patch.cookies,
     patch.encoding,
     patch.encoding,
     patch.headers,
     patch.headers,
   );
   );
@@ -161,7 +164,7 @@ function createRequestStream(secure, body, encoding) {
   if (encoding != null && typeof encoding !== 'string')
   if (encoding != null && typeof encoding !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "encoding" of "createRequestStream" ' +
       'The parameter "encoding" of "createRequestStream" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       encoding,
       encoding,
     );
     );
   encoding = encoding || 'utf-8';
   encoding = encoding || 'utf-8';
@@ -198,7 +201,7 @@ function createRequestUrl(path, query) {
   if (typeof path !== 'string')
   if (typeof path !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "path" of "createRequestUrl" ' +
       'The parameter "path" of "createRequestUrl" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       path,
       path,
     );
     );
   if (
   if (
@@ -207,7 +210,7 @@ function createRequestUrl(path, query) {
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
       'The parameter "query" of "createRequestUrl" ' +
       'The parameter "query" of "createRequestUrl" ' +
-        'should be a String or Object, but %v given.',
+        'should be a String or Object, but %v was given.',
       query,
       query,
     );
     );
   }
   }
@@ -227,36 +230,36 @@ function createRequestUrl(path, query) {
  * @param {string|null|undefined} host
  * @param {string|null|undefined} host
  * @param {boolean|null|undefined} secure
  * @param {boolean|null|undefined} secure
  * @param {*} body
  * @param {*} body
- * @param {string|object|null|undefined} cookie
+ * @param {string|object|null|undefined} cookies
  * @param {import('buffer').BufferEncoding|null|undefined} encoding
  * @param {import('buffer').BufferEncoding|null|undefined} encoding
  * @param {object|null|undefined} headers
  * @param {object|null|undefined} headers
  * @returns {object}
  * @returns {object}
  */
  */
-function createRequestHeaders(host, secure, body, cookie, encoding, headers) {
+function createRequestHeaders(host, secure, body, cookies, encoding, headers) {
   if (host != null && typeof host !== 'string')
   if (host != null && typeof host !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "host" of "createRequestHeaders" ' +
       'The parameter "host" of "createRequestHeaders" ' +
-        'a non-empty String, but %v given.',
+        'a non-empty String, but %v was given.',
       host,
       host,
     );
     );
   host = host || 'localhost';
   host = host || 'localhost';
   if (secure != null && typeof secure !== 'boolean')
   if (secure != null && typeof secure !== 'boolean')
     throw new Errorf(
     throw new Errorf(
       'The parameter "secure" of "createRequestHeaders" ' +
       'The parameter "secure" of "createRequestHeaders" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       secure,
       secure,
     );
     );
   secure = Boolean(secure);
   secure = Boolean(secure);
   if (
   if (
-    (cookie != null &&
-      typeof cookie !== 'object' &&
-      typeof cookie !== 'string') ||
-    Array.isArray(cookie)
+    (cookies != null &&
+      typeof cookies !== 'object' &&
+      typeof cookies !== 'string') ||
+    Array.isArray(cookies)
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
-      'The parameter "cookie" of "createRequestHeaders" ' +
-        'should be a String or Object, but %v given.',
-      cookie,
+      'The parameter "cookies" of "createRequestHeaders" ' +
+        'should be a String or Object, but %v was given.',
+      cookies,
     );
     );
   }
   }
   if (
   if (
@@ -265,7 +268,7 @@ function createRequestHeaders(host, secure, body, cookie, encoding, headers) {
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
       'The parameter "headers" of "createRequestHeaders" ' +
       'The parameter "headers" of "createRequestHeaders" ' +
-        'should be an Object, but %v given.',
+        'should be an Object, but %v was given.',
       headers,
       headers,
     );
     );
   }
   }
@@ -273,7 +276,7 @@ function createRequestHeaders(host, secure, body, cookie, encoding, headers) {
   if (encoding != null && typeof encoding !== 'string')
   if (encoding != null && typeof encoding !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "encoding" of "createRequestHeaders" ' +
       'The parameter "encoding" of "createRequestHeaders" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       encoding,
       encoding,
     );
     );
   encoding = encoding || 'utf-8';
   encoding = encoding || 'utf-8';
@@ -282,13 +285,14 @@ function createRequestHeaders(host, secure, body, cookie, encoding, headers) {
   if (secure) obj['x-forwarded-proto'] = 'https';
   if (secure) obj['x-forwarded-proto'] = 'https';
   // формирование заголовка Cookie
   // формирование заголовка Cookie
   // из строки или объекта
   // из строки или объекта
-  if (cookie != null) {
-    if (typeof cookie === 'string') {
+  if (cookies != null) {
+    if (typeof cookies === 'string') {
       obj['cookie'] = obj['cookie'] ? obj['cookie'] : '';
       obj['cookie'] = obj['cookie'] ? obj['cookie'] : '';
-      obj['cookie'] += cookie;
-    } else if (typeof cookie === 'object') {
+      obj['cookie'] += obj['cookie'] ? `; ${cookies}` : cookies;
+    } else if (typeof cookies === 'object') {
       obj['cookie'] = obj['cookie'] ? obj['cookie'] : '';
       obj['cookie'] = obj['cookie'] ? obj['cookie'] : '';
-      obj['cookie'] += createCookieString(cookie);
+      const newCookies = createCookiesString(cookies);
+      obj['cookie'] += obj['cookie'] ? `; ${newCookies}` : newCookies;
     }
     }
   }
   }
   // установка заголовка "content-type"
   // установка заголовка "content-type"

+ 18 - 18
src/utils/create-request-mock.spec.js

@@ -4,7 +4,7 @@ import {TLSSocket} from 'tls';
 import {expect} from '../chai.js';
 import {expect} from '../chai.js';
 import {format} from '@e22m4u/js-format';
 import {format} from '@e22m4u/js-format';
 import {createRequestMock} from './create-request-mock.js';
 import {createRequestMock} from './create-request-mock.js';
-import {BUFFER_ENCODING_LIST} from './fetch-request-body.js';
+import {CHARACTER_ENCODING_LIST} from './fetch-request-body.js';
 
 
 describe('createRequestMock', function () {
 describe('createRequestMock', function () {
   it('requires the first argument to be an Object', function () {
   it('requires the first argument to be an Object', function () {
@@ -12,7 +12,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first parameter of "createRequestMock" ' +
         'The first parameter of "createRequestMock" ' +
-          'should be an Object, but %s given.',
+          'should be an Object, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -32,7 +32,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "host" of "createRequestMock" ' +
         'The parameter "host" of "createRequestMock" ' +
-          'should be a String, but %s given.',
+          'should be a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -52,7 +52,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "method" of "createRequestMock" ' +
         'The parameter "method" of "createRequestMock" ' +
-          'should be a String, but %s given.',
+          'should be a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -72,7 +72,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "secure" of "createRequestMock" ' +
         'The parameter "secure" of "createRequestMock" ' +
-          'should be a Boolean, but %s given.',
+          'should be a Boolean, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -92,7 +92,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "path" of "createRequestMock" ' +
         'The parameter "path" of "createRequestMock" ' +
-          'should be a String, but %s given.',
+          'should be a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -112,7 +112,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "query" of "createRequestMock" ' +
         'The parameter "query" of "createRequestMock" ' +
-          'should be a String or Object, but %s given.',
+          'should be a String or Object, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -128,12 +128,12 @@ describe('createRequestMock', function () {
     throwable(null)();
     throwable(null)();
   });
   });
 
 
-  it('requires the parameter "cookie" to be a String or Object', function () {
-    const throwable = v => () => createRequestMock({cookie: v});
+  it('requires the parameter "cookies" to be a String or Object', function () {
+    const throwable = v => () => createRequestMock({cookies: v});
     const error = v =>
     const error = v =>
       format(
       format(
-        'The parameter "cookie" of "createRequestMock" ' +
-          'should be a String or Object, but %s given.',
+        'The parameter "cookies" of "createRequestMock" ' +
+          'should be a String or Object, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -154,7 +154,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "headers" of "createRequestMock" ' +
         'The parameter "headers" of "createRequestMock" ' +
-          'should be an Object, but %s given.',
+          'should be an Object, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -175,7 +175,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "stream" of "createRequestMock" ' +
         'The parameter "stream" of "createRequestMock" ' +
-          'should be a Stream, but %s given.',
+          'should be a Stream, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -196,7 +196,7 @@ describe('createRequestMock', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "encoding" of "createRequestMock" ' +
         'The parameter "encoding" of "createRequestMock" ' +
-          'should be a String, but %s given.',
+          'should be a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -212,10 +212,10 @@ describe('createRequestMock', function () {
 
 
   it('requires the parameter "encoding" to be the BufferEncoding', function () {
   it('requires the parameter "encoding" to be the BufferEncoding', function () {
     const throwable = v => () => createRequestMock({encoding: v});
     const throwable = v => () => createRequestMock({encoding: v});
-    const error = v => format('Buffer encoding %s is not supported.', v);
+    const error = v => format('Character encoding %s is not supported.', v);
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('')).to.throw(error('""'));
     expect(throwable('')).to.throw(error('""'));
-    BUFFER_ENCODING_LIST.forEach(v => throwable(v)());
+    CHARACTER_ENCODING_LIST.forEach(v => throwable(v)());
   });
   });
 
 
   it('does not allow the option "secure" with the "stream"', function () {
   it('does not allow the option "secure" with the "stream"', function () {
@@ -389,12 +389,12 @@ describe('createRequestMock', function () {
   });
   });
 
 
   it('sets the header "cookie" from a String', function () {
   it('sets the header "cookie" from a String', function () {
-    const req = createRequestMock({cookie: 'test'});
+    const req = createRequestMock({cookies: 'test'});
     expect(req.headers['cookie']).to.be.eq('test');
     expect(req.headers['cookie']).to.be.eq('test');
   });
   });
 
 
   it('sets the header "cookie" from an Object', function () {
   it('sets the header "cookie" from an Object', function () {
-    const req = createRequestMock({cookie: {p1: 'foo', p2: 'bar'}});
+    const req = createRequestMock({cookies: {p1: 'foo', p2: 'bar'}});
     expect(req.headers['cookie']).to.be.eq('p1=foo; p2=bar;');
     expect(req.headers['cookie']).to.be.eq('p1=foo; p2=bar;');
   });
   });
 
 

+ 11 - 2
src/utils/fetch-request-body.d.ts

@@ -1,9 +1,18 @@
 import {IncomingMessage} from 'http';
 import {IncomingMessage} from 'http';
 
 
 /**
 /**
- * Buffer encoding list.
+ * Character encoding list.
  */
  */
-export type BUFFER_ENCODING_LIST = BufferEncoding[];
+export const CHARACTER_ENCODING_LIST: (
+  | 'ascii'
+  | 'utf8'
+  | 'utf-8'
+  | 'utf16le'
+  | 'utf-16le'
+  | 'ucs2'
+  | 'ucs-2'
+  | 'latin1'
+)[];
 
 
 /**
 /**
  * Fetch request body.
  * Fetch request body.

+ 10 - 17
src/utils/fetch-request-body.js

@@ -1,15 +1,13 @@
 import HttpErrors from 'http-errors';
 import HttpErrors from 'http-errors';
+import {IncomingMessage} from 'http';
+import {Errorf} from '@e22m4u/js-format';
 import {createError} from './create-error.js';
 import {createError} from './create-error.js';
 import {parseContentType} from './parse-content-type.js';
 import {parseContentType} from './parse-content-type.js';
-import {Errorf} from '@e22m4u/js-format';
-import {IncomingMessage} from 'http';
 
 
 /**
 /**
- * Buffer encoding.
- *
- * @type {import('buffer').BufferEncoding[]}
+ * Character encoding list.
  */
  */
-export const BUFFER_ENCODING_LIST = [
+export const CHARACTER_ENCODING_LIST = [
   'ascii',
   'ascii',
   'utf8',
   'utf8',
   'utf-8',
   'utf-8',
@@ -17,11 +15,7 @@ export const BUFFER_ENCODING_LIST = [
   'utf-16le',
   'utf-16le',
   'ucs2',
   'ucs2',
   'ucs-2',
   'ucs-2',
-  'base64',
-  'base64url',
   'latin1',
   'latin1',
-  'binary',
-  'hex',
 ];
 ];
 
 
 /**
 /**
@@ -35,13 +29,13 @@ export function fetchRequestBody(req, bodyBytesLimit = 0) {
   if (!(req instanceof IncomingMessage))
   if (!(req instanceof IncomingMessage))
     throw new Errorf(
     throw new Errorf(
       'The first parameter of "fetchRequestBody" should be ' +
       'The first parameter of "fetchRequestBody" should be ' +
-        'an IncomingMessage instance, but %v given.',
+        'an IncomingMessage instance, but %v was given.',
       req,
       req,
     );
     );
   if (typeof bodyBytesLimit !== 'number')
   if (typeof bodyBytesLimit !== 'number')
     throw new Errorf(
     throw new Errorf(
       'The parameter "bodyBytesLimit" of "fetchRequestBody" ' +
       'The parameter "bodyBytesLimit" of "fetchRequestBody" ' +
-        'should be a number, but %v given.',
+        'should be a number, but %v was given.',
       bodyBytesLimit,
       bodyBytesLimit,
     );
     );
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
@@ -64,7 +58,7 @@ export function fetchRequestBody(req, bodyBytesLimit = 0) {
       const parsedContentType = parseContentType(contentType);
       const parsedContentType = parseContentType(contentType);
       if (parsedContentType && parsedContentType.charset) {
       if (parsedContentType && parsedContentType.charset) {
         encoding = parsedContentType.charset.toLowerCase();
         encoding = parsedContentType.charset.toLowerCase();
-        if (!BUFFER_ENCODING_LIST.includes(encoding))
+        if (!CHARACTER_ENCODING_LIST.includes(encoding))
           throw createError(
           throw createError(
             HttpErrors.UnsupportedMediaType,
             HttpErrors.UnsupportedMediaType,
             'Request encoding %v is not supported.',
             'Request encoding %v is not supported.',
@@ -106,12 +100,11 @@ export function fetchRequestBody(req, bodyBytesLimit = 0) {
         reject(error);
         reject(error);
         return;
         return;
       }
       }
-      // объединение массива байтов в буфер,
-      // кодирование результата в строку,
-      // и передача полученных данных
+      // объединение массива байтов в буфер, кодирование
+      // результата в строку, и передача полученных данных
       // в ожидающий Promise
       // в ожидающий Promise
       const buffer = Buffer.concat(data);
       const buffer = Buffer.concat(data);
-      const body = Buffer.from(buffer, encoding).toString();
+      const body = buffer.toString(encoding);
       resolve(body || undefined);
       resolve(body || undefined);
     };
     };
     // при ошибке загрузки тела запроса,
     // при ошибке загрузки тела запроса,

+ 8 - 9
src/utils/fetch-request-body.spec.js

@@ -9,7 +9,7 @@ describe('fetchRequestBody', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first parameter of "fetchRequestBody" should be ' +
         'The first parameter of "fetchRequestBody" should be ' +
-          'an IncomingMessage instance, but %s given.',
+          'an IncomingMessage instance, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -31,7 +31,7 @@ describe('fetchRequestBody', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The parameter "bodyBytesLimit" of "fetchRequestBody" ' +
         'The parameter "bodyBytesLimit" of "fetchRequestBody" ' +
-          'should be a number, but %s given.',
+          'should be a number, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));
@@ -83,16 +83,15 @@ describe('fetchRequestBody', function () {
       expect(result).to.be.eq(body);
       expect(result).to.be.eq(body);
     });
     });
 
 
-    it('decodes non-standard encoding', async function () {
-      const body = 'Lorem Ipsum is simply dummy text.';
-      const encoding = 'base64';
-      const encodedBody = Buffer.from(body).toString(encoding);
+    it('decodes non-UTF-8 encoding to a plain text', async function () {
+      const originalBody = 'Hello, world!';
       const req = createRequestMock({
       const req = createRequestMock({
-        body: Buffer.from(encodedBody, encoding),
-        headers: {'content-type': `text/plain; charset=${encoding}`},
+        body: originalBody,
+        encoding: 'latin1',
+        headers: {'content-type': `text/plain; charset=latin1`},
       });
       });
       const result = await fetchRequestBody(req);
       const result = await fetchRequestBody(req);
-      expect(result).to.be.eq(body);
+      expect(result).to.be.eq(originalBody);
     });
     });
   });
   });
 
 

+ 1 - 1
src/utils/get-request-pathname.js

@@ -15,7 +15,7 @@ export function getRequestPathname(req) {
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
       'The first argument of "getRequestPathname" should be ' +
       'The first argument of "getRequestPathname" should be ' +
-        'an instance of IncomingMessage, but %v given.',
+        'an instance of IncomingMessage, but %v was given.',
       req,
       req,
     );
     );
   }
   }

+ 1 - 1
src/utils/get-request-pathname.spec.js

@@ -8,7 +8,7 @@ describe('getRequestPathname', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first argument of "getRequestPathname" should be ' +
         'The first argument of "getRequestPathname" should be ' +
-          'an instance of IncomingMessage, but %s given.',
+          'an instance of IncomingMessage, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));

+ 2 - 2
src/utils/index.d.ts

@@ -1,6 +1,6 @@
 export * from './is-promise.js';
 export * from './is-promise.js';
-export * from './parse-cookie.js';
 export * from './create-error.js';
 export * from './create-error.js';
+export * from './parse-cookies.js';
 export * from './to-camel-case.js';
 export * from './to-camel-case.js';
 export * from './create-debugger.js';
 export * from './create-debugger.js';
 export * from './is-response-sent.js';
 export * from './is-response-sent.js';
@@ -10,5 +10,5 @@ export * from './is-writable-stream.js';
 export * from './fetch-request-body.js';
 export * from './fetch-request-body.js';
 export * from './create-request-mock.js';
 export * from './create-request-mock.js';
 export * from './create-response-mock.js';
 export * from './create-response-mock.js';
-export * from './create-cookie-string.js';
+export * from './create-cookies-string.js';
 export * from './get-request-pathname.js';
 export * from './get-request-pathname.js';

+ 2 - 2
src/utils/index.js

@@ -1,6 +1,6 @@
 export * from './is-promise.js';
 export * from './is-promise.js';
-export * from './parse-cookie.js';
 export * from './create-error.js';
 export * from './create-error.js';
+export * from './parse-cookies.js';
 export * from './to-camel-case.js';
 export * from './to-camel-case.js';
 export * from './create-debugger.js';
 export * from './create-debugger.js';
 export * from './is-response-sent.js';
 export * from './is-response-sent.js';
@@ -10,5 +10,5 @@ export * from './is-writable-stream.js';
 export * from './fetch-request-body.js';
 export * from './fetch-request-body.js';
 export * from './create-request-mock.js';
 export * from './create-request-mock.js';
 export * from './create-response-mock.js';
 export * from './create-response-mock.js';
-export * from './create-cookie-string.js';
+export * from './create-cookies-string.js';
 export * from './get-request-pathname.js';
 export * from './get-request-pathname.js';

+ 1 - 2
src/utils/is-readable-stream.js

@@ -1,6 +1,5 @@
 /**
 /**
- * Check whether a value has a pipe
- * method.
+ * Check whether a value has a pipe method.
  *
  *
  * @param {*} value
  * @param {*} value
  * @returns {boolean}
  * @returns {boolean}

+ 1 - 1
src/utils/is-response-sent.js

@@ -15,7 +15,7 @@ export function isResponseSent(res) {
   ) {
   ) {
     throw new Errorf(
     throw new Errorf(
       'The first argument of "isResponseSent" should be ' +
       'The first argument of "isResponseSent" should be ' +
-        'an instance of ServerResponse, but %v given.',
+        'an instance of ServerResponse, but %v was given.',
       res,
       res,
     );
     );
   }
   }

+ 1 - 1
src/utils/is-response-sent.spec.js

@@ -8,7 +8,7 @@ describe('isResponseSent', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first argument of "isResponseSent" should be ' +
         'The first argument of "isResponseSent" should be ' +
-          'an instance of ServerResponse, but %s given.',
+          'an instance of ServerResponse, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable('str')).to.throw(error('"str"'));
     expect(throwable('str')).to.throw(error('"str"'));

+ 1 - 1
src/utils/parse-content-type.js

@@ -14,7 +14,7 @@ export function parseContentType(input) {
   if (typeof input !== 'string')
   if (typeof input !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The parameter "input" of "parseContentType" ' +
       'The parameter "input" of "parseContentType" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       input,
       input,
     );
     );
   const res = {mediaType: undefined, charset: undefined, boundary: undefined};
   const res = {mediaType: undefined, charset: undefined, boundary: undefined};

+ 0 - 19
src/utils/parse-cookie.d.ts

@@ -1,19 +0,0 @@
-/**
- * Parsed cookie.
- */
-type ParsedCookie = {
-  [key: string]: string | undefined;
-};
-
-/**
- * Parse cookie.
- *
- * @example
- * ```ts
- * parseCookie('pkg=math; equation=E%3Dmc%5E2');
- * // {pkg: 'math', equation: 'E=mc^2'}
- * ```
- *
- * @param input
- */
-export declare function parseCookie(input: string): ParsedCookie;

+ 19 - 0
src/utils/parse-cookies.d.ts

@@ -0,0 +1,19 @@
+/**
+ * Parsed cookies.
+ */
+type ParsedCookies = {
+  [key: string]: string | undefined;
+};
+
+/**
+ * Parse cookies.
+ *
+ * @example
+ * ```ts
+ * parseCookies('pkg=math; equation=E%3Dmc%5E2');
+ * // {pkg: 'math', equation: 'E=mc^2'}
+ * ```
+ *
+ * @param input
+ */
+export declare function parseCookies(input: string): ParsedCookies;

+ 8 - 5
src/utils/parse-cookie.js → src/utils/parse-cookies.js

@@ -1,21 +1,22 @@
 import {Errorf} from '@e22m4u/js-format';
 import {Errorf} from '@e22m4u/js-format';
 
 
 /**
 /**
- * Parse cookie.
+ * Parse cookies.
  *
  *
  * @example
  * @example
  * ```ts
  * ```ts
- * parseCookie('pkg=math; equation=E%3Dmc%5E2');
+ * parseCookies('pkg=math; equation=E%3Dmc%5E2');
  * // {pkg: 'math', equation: 'E=mc^2'}
  * // {pkg: 'math', equation: 'E=mc^2'}
  * ```
  * ```
  *
  *
  * @param {string} input
  * @param {string} input
  * @returns {object}
  * @returns {object}
  */
  */
-export function parseCookie(input) {
+export function parseCookies(input) {
   if (typeof input !== 'string')
   if (typeof input !== 'string')
     throw new Errorf(
     throw new Errorf(
-      'The first parameter of "parseCookie" should be a String, but %v given.',
+      'The first parameter of "parseCookies" should be a String, ' +
+        'but %v was given.',
       input,
       input,
     );
     );
   return input
   return input
@@ -24,7 +25,9 @@ export function parseCookie(input) {
     .map(v => v.split('='))
     .map(v => v.split('='))
     .reduce((cookies, tuple) => {
     .reduce((cookies, tuple) => {
       const key = decodeURIComponent(tuple[0]).trim();
       const key = decodeURIComponent(tuple[0]).trim();
-      cookies[key] = decodeURIComponent(tuple[1]).trim();
+      const value =
+        tuple[1] !== undefined ? decodeURIComponent(tuple[1]).trim() : '';
+      cookies[key] = value;
       return cookies;
       return cookies;
     }, {});
     }, {});
 }
 }

+ 13 - 8
src/utils/parse-cookie.spec.js → src/utils/parse-cookies.spec.js

@@ -1,14 +1,14 @@
 import {expect} from '../chai.js';
 import {expect} from '../chai.js';
 import {format} from '@e22m4u/js-format';
 import {format} from '@e22m4u/js-format';
-import {parseCookie} from './parse-cookie.js';
+import {parseCookies} from './parse-cookies.js';
 
 
-describe('parseCookie', function () {
+describe('parseCookies', function () {
   it('requires the first parameter to be an IncomingMessage instance', function () {
   it('requires the first parameter to be an IncomingMessage instance', function () {
-    const throwable = v => () => parseCookie(v);
+    const throwable = v => () => parseCookies(v);
     const error = v =>
     const error = v =>
       format(
       format(
-        'The first parameter of "parseCookie" should be ' +
-          'a String, but %s given.',
+        'The first parameter of "parseCookies" should be ' +
+          'a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));
@@ -23,15 +23,20 @@ describe('parseCookie', function () {
     throwable('')();
     throwable('')();
   });
   });
 
 
-  it('returns cookie parameters', function () {
+  it('returns cookies as a plain object', function () {
     const value = 'pkg=math; equation=E%3Dmc%5E2';
     const value = 'pkg=math; equation=E%3Dmc%5E2';
-    const result = parseCookie(value);
+    const result = parseCookies(value);
     expect(result).to.have.property('pkg', 'math');
     expect(result).to.have.property('pkg', 'math');
     expect(result).to.have.property('equation', 'E=mc^2');
     expect(result).to.have.property('equation', 'E=mc^2');
   });
   });
 
 
   it('returns an empty object for an empty string', function () {
   it('returns an empty object for an empty string', function () {
-    const result = parseCookie('');
+    const result = parseCookies('');
     expect(result).to.be.eql({});
     expect(result).to.be.eql({});
   });
   });
+
+  it('parses an empty cookie as an empty string', function () {
+    const result = parseCookies('foo=bar; baz');
+    expect(result).to.be.eql({foo: 'bar', baz: ''});
+  });
 });
 });

+ 1 - 1
src/utils/to-camel-case.js

@@ -10,7 +10,7 @@ export function toCamelCase(input) {
   if (typeof input !== 'string')
   if (typeof input !== 'string')
     throw new Errorf(
     throw new Errorf(
       'The first argument of "toCamelCase" ' +
       'The first argument of "toCamelCase" ' +
-        'should be a String, but %v given.',
+        'should be a String, but %v was given.',
       input,
       input,
     );
     );
   return input
   return input

+ 1 - 1
src/utils/to-camel-case.spec.js

@@ -8,7 +8,7 @@ describe('toCamelCase', function () {
     const error = v =>
     const error = v =>
       format(
       format(
         'The first argument of "toCamelCase" ' +
         'The first argument of "toCamelCase" ' +
-          'should be a String, but %s given.',
+          'should be a String, but %s was given.',
         v,
         v,
       );
       );
     expect(throwable(10)).to.throw(error('10'));
     expect(throwable(10)).to.throw(error('10'));

Некоторые файлы не были показаны из-за большого количества измененных файлов