@e22m4u/js-trie-router 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/cjs/index.cjs +13 -13
- package/package.json +1 -1
- package/src/parsers/request-body-parser.d.ts +2 -2
- package/src/parsers/request-body-parser.js +12 -12
- package/src/parsers/request-body-parser.spec.js +2 -2
- package/src/utils/create-response-mock.js +3 -1
- package/src/utils/create-response-mock.spec.js +16 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ HTTP маршрутизатор для Node.js на основе
|
|
|
20
20
|
- [Параметры маршрутизатора](#параметры-маршрутизатора)
|
|
21
21
|
- [Контекст запроса](#контекст-запроса)
|
|
22
22
|
- [Отправка ответа](#отправка-ответа)
|
|
23
|
+
- [Парсинг тела запроса](#парсинг-тела-запроса)
|
|
23
24
|
- [Жизненный цикл](#жизненный-цикл)
|
|
24
25
|
- [Хуки маршрута](#хуки-маршрута)
|
|
25
26
|
- [Глобальные хуки](#глобальные-хуки)
|
|
@@ -219,6 +220,99 @@ router.defineRoute({
|
|
|
219
220
|
});
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
### Парсинг тела запроса
|
|
224
|
+
|
|
225
|
+
Для разбора тела входящего запроса отслеживается заголовок `Content-Type`,
|
|
226
|
+
определяющий формат передаваемых данных. По умолчанию маршрутизатор включает
|
|
227
|
+
парсеры для следующих форматов:
|
|
228
|
+
|
|
229
|
+
- `application/json` разбирается как *JSON*;
|
|
230
|
+
- `text/plain` преобразуется в строку;
|
|
231
|
+
|
|
232
|
+
Если входящий запрос содержит данные, но для его формата не найден подходящий
|
|
233
|
+
парсер, маршрутизатор прервет обработку запроса и вернет ошибку
|
|
234
|
+
*415 Unsupported Media Type*.
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"error": {
|
|
239
|
+
"message": "Media type \"application/octet-stream\" is not supported."
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Чтобы избежать появления ошибки для форматов, которые предполагается
|
|
245
|
+
обрабатывать особым способом, предусмотрен параметр маршрутизатора
|
|
246
|
+
`ignoredMediaTypes` для игнорирования указанных медиа-типов. Параметр
|
|
247
|
+
позволяет пропустить встроенный парсинг, как это сделано в примере ниже.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import {TrieRouter, HttpMethod} from '@e22m4u/js-trie-router';
|
|
251
|
+
|
|
252
|
+
// создание экземпляра маршрутизатора
|
|
253
|
+
// с указанием исключаемых медиа-типов
|
|
254
|
+
const router = new TrieRouter({
|
|
255
|
+
ignoredMediaTypes: [
|
|
256
|
+
'application/octet-stream',
|
|
257
|
+
'multipart/form-data'
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// регистрация маршрута для обработки файлов
|
|
262
|
+
router.defineRoute({
|
|
263
|
+
method: HttpMethod.POST,
|
|
264
|
+
path: '/upload',
|
|
265
|
+
handler(ctx) {
|
|
266
|
+
// свойство body остается пустым, далее
|
|
267
|
+
// выполняется доступ к нативному потоку
|
|
268
|
+
const stream = ctx.request;
|
|
269
|
+
return 'OK';
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### Регистрация пользовательского парсера
|
|
275
|
+
|
|
276
|
+
Для расширения поддерживаемых форматов предусмотрена возможность регистрации
|
|
277
|
+
пользовательской функции парсинга для определенного медиа-типа. Управление
|
|
278
|
+
такими функциями выполняется сервисом `RequestBodyParser`, который доступен
|
|
279
|
+
через глобальный контейнер маршрутизатора.
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
// доступ к сервису через маршрутизатор
|
|
283
|
+
const bodyParser = router.getService(RequestBodyParser);
|
|
284
|
+
// bodyParser.defineParser(mediaType, parserFn); см. далее
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Регистрируемая функция принимает извлеченные данные в виде строки и возвращает
|
|
288
|
+
преобразованный результат. Итоговое значение впоследствии будет доступно
|
|
289
|
+
в контексте обработки запроса.
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import queryString from 'querystring';
|
|
293
|
+
import {TrieRouter, HttpMethod, RequestBodyParser} from '@e22m4u/js-trie-router';
|
|
294
|
+
|
|
295
|
+
const router = new TrieRouter();
|
|
296
|
+
const bodyParser = router.getService(RequestBodyParser);
|
|
297
|
+
|
|
298
|
+
// регистрация парсера для обработки данных формы
|
|
299
|
+
bodyParser.defineParser(
|
|
300
|
+
'application/x-www-form-urlencoded',
|
|
301
|
+
(input) => queryString.parse(input),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// определение маршрута
|
|
305
|
+
router.defineRoute({
|
|
306
|
+
method: HttpMethod.POST,
|
|
307
|
+
path: '/submit',
|
|
308
|
+
handler(ctx) {
|
|
309
|
+
// свойство содержит результат
|
|
310
|
+
// работы новой парсер-функции
|
|
311
|
+
return ctx.body;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
222
316
|
### Жизненный цикл
|
|
223
317
|
|
|
224
318
|
Для понимания того, как маршрутизатор обрабатывает входящий запрос, ниже
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -881,7 +881,7 @@ function patchHeaders(response) {
|
|
|
881
881
|
);
|
|
882
882
|
}
|
|
883
883
|
const key = name.toLowerCase();
|
|
884
|
-
this._headers[key] = String(value);
|
|
884
|
+
this._headers[key] = Array.isArray(value) ? [...value.map(String)] : String(value);
|
|
885
885
|
return this;
|
|
886
886
|
}, "value")
|
|
887
887
|
});
|
|
@@ -2156,23 +2156,23 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2156
2156
|
* Set parser.
|
|
2157
2157
|
*
|
|
2158
2158
|
* @param {string} mediaType
|
|
2159
|
-
* @param {Function}
|
|
2159
|
+
* @param {Function} parserFn
|
|
2160
2160
|
* @returns {this}
|
|
2161
2161
|
*/
|
|
2162
|
-
defineParser(mediaType,
|
|
2162
|
+
defineParser(mediaType, parserFn) {
|
|
2163
2163
|
if (!mediaType || typeof mediaType !== "string") {
|
|
2164
2164
|
throw new import_js_format19.InvalidArgumentError(
|
|
2165
2165
|
'Parameter "mediaType" must be a non-empty String, but %v was given.',
|
|
2166
2166
|
mediaType
|
|
2167
2167
|
);
|
|
2168
2168
|
}
|
|
2169
|
-
if (!
|
|
2169
|
+
if (!parserFn || typeof parserFn !== "function") {
|
|
2170
2170
|
throw new import_js_format19.InvalidArgumentError(
|
|
2171
|
-
'Parameter "
|
|
2172
|
-
|
|
2171
|
+
'Parameter "parserFn" must be a Function, but %v was given.',
|
|
2172
|
+
parserFn
|
|
2173
2173
|
);
|
|
2174
2174
|
}
|
|
2175
|
-
this._parsers[mediaType.toLowerCase()] =
|
|
2175
|
+
this._parsers[mediaType.toLowerCase()] = parserFn;
|
|
2176
2176
|
return this;
|
|
2177
2177
|
}
|
|
2178
2178
|
/**
|
|
@@ -2203,14 +2203,14 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2203
2203
|
mediaType
|
|
2204
2204
|
);
|
|
2205
2205
|
}
|
|
2206
|
-
const
|
|
2207
|
-
if (!
|
|
2206
|
+
const parserFn = this._parsers[mediaType.toLowerCase()];
|
|
2207
|
+
if (!parserFn) {
|
|
2208
2208
|
throw new import_js_format19.InvalidArgumentError(
|
|
2209
2209
|
"Media type %v does not have a parser.",
|
|
2210
2210
|
mediaType
|
|
2211
2211
|
);
|
|
2212
2212
|
}
|
|
2213
|
-
return
|
|
2213
|
+
return parserFn;
|
|
2214
2214
|
}
|
|
2215
2215
|
/**
|
|
2216
2216
|
* Remove parser.
|
|
@@ -2264,8 +2264,8 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2264
2264
|
debug("Media type %v is ignored.", mediaType);
|
|
2265
2265
|
return;
|
|
2266
2266
|
}
|
|
2267
|
-
const
|
|
2268
|
-
if (!
|
|
2267
|
+
const parserFn = this._parsers[mediaTypeLc];
|
|
2268
|
+
if (!parserFn) {
|
|
2269
2269
|
throw createError(
|
|
2270
2270
|
import_http_errors2.default.UnsupportedMediaType,
|
|
2271
2271
|
"Media type %v is not supported.",
|
|
@@ -2278,7 +2278,7 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2278
2278
|
return fetchRequestBody(request, bodyBytesLimit).then((rawBody) => {
|
|
2279
2279
|
if (rawBody != null) {
|
|
2280
2280
|
debug("Read %v bytes.", Buffer.byteLength(rawBody, "utf8"));
|
|
2281
|
-
return
|
|
2281
|
+
return parserFn(rawBody);
|
|
2282
2282
|
}
|
|
2283
2283
|
debug("Request body has no content.");
|
|
2284
2284
|
return rawBody;
|
package/package.json
CHANGED
|
@@ -15,9 +15,9 @@ export declare class RequestBodyParser extends DebuggableService {
|
|
|
15
15
|
* Define parser.
|
|
16
16
|
*
|
|
17
17
|
* @param mediaType
|
|
18
|
-
* @param
|
|
18
|
+
* @param parserFn
|
|
19
19
|
*/
|
|
20
|
-
defineParser(mediaType: string,
|
|
20
|
+
defineParser(mediaType: string, parserFn: BodyParserFunction): this;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Has parser.
|
|
@@ -29,10 +29,10 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
29
29
|
* Set parser.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} mediaType
|
|
32
|
-
* @param {Function}
|
|
32
|
+
* @param {Function} parserFn
|
|
33
33
|
* @returns {this}
|
|
34
34
|
*/
|
|
35
|
-
defineParser(mediaType,
|
|
35
|
+
defineParser(mediaType, parserFn) {
|
|
36
36
|
if (!mediaType || typeof mediaType !== 'string') {
|
|
37
37
|
throw new InvalidArgumentError(
|
|
38
38
|
'Parameter "mediaType" must be a non-empty String, ' +
|
|
@@ -40,13 +40,13 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
40
40
|
mediaType,
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
-
if (!
|
|
43
|
+
if (!parserFn || typeof parserFn !== 'function') {
|
|
44
44
|
throw new InvalidArgumentError(
|
|
45
|
-
'Parameter "
|
|
46
|
-
|
|
45
|
+
'Parameter "parserFn" must be a Function, but %v was given.',
|
|
46
|
+
parserFn,
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
this._parsers[mediaType.toLowerCase()] =
|
|
49
|
+
this._parsers[mediaType.toLowerCase()] = parserFn;
|
|
50
50
|
return this;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -81,14 +81,14 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
81
81
|
mediaType,
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
84
|
+
const parserFn = this._parsers[mediaType.toLowerCase()];
|
|
85
|
+
if (!parserFn) {
|
|
86
86
|
throw new InvalidArgumentError(
|
|
87
87
|
'Media type %v does not have a parser.',
|
|
88
88
|
mediaType,
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
|
-
return
|
|
91
|
+
return parserFn;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
@@ -155,8 +155,8 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
155
155
|
}
|
|
156
156
|
// если парсер для текущего медиа типа
|
|
157
157
|
// не определен, то выбрасывается ошибка
|
|
158
|
-
const
|
|
159
|
-
if (!
|
|
158
|
+
const parserFn = this._parsers[mediaTypeLc];
|
|
159
|
+
if (!parserFn) {
|
|
160
160
|
throw createError(
|
|
161
161
|
HttpErrors.UnsupportedMediaType,
|
|
162
162
|
'Media type %v is not supported.',
|
|
@@ -173,7 +173,7 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
173
173
|
return fetchRequestBody(request, bodyBytesLimit).then(rawBody => {
|
|
174
174
|
if (rawBody != null) {
|
|
175
175
|
debug('Read %v bytes.', Buffer.byteLength(rawBody, 'utf8'));
|
|
176
|
-
return
|
|
176
|
+
return parserFn(rawBody);
|
|
177
177
|
}
|
|
178
178
|
debug('Request body has no content.');
|
|
179
179
|
return rawBody;
|
|
@@ -30,11 +30,11 @@ describe('RequestBodyParser', function () {
|
|
|
30
30
|
throwable('text/plain')();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('should require the parameter "
|
|
33
|
+
it('should require the parameter "parserFn" to be a Function', function () {
|
|
34
34
|
const S = new RequestBodyParser();
|
|
35
35
|
const throwable = v => () => S.defineParser('str', v);
|
|
36
36
|
const error = v =>
|
|
37
|
-
format('Parameter "
|
|
37
|
+
format('Parameter "parserFn" must be a Function, but %s was given.', v);
|
|
38
38
|
expect(throwable('str')).to.throw(error('"str"'));
|
|
39
39
|
expect(throwable('')).to.throw(error('""'));
|
|
40
40
|
expect(throwable(10)).to.throw(error('10'));
|
|
@@ -79,6 +79,22 @@ describe('createResponseMock', function () {
|
|
|
79
79
|
expect(ret).to.be.eq(res);
|
|
80
80
|
expect(res._headers['num']).to.be.eq('10');
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
it('should not stringify an array value', function () {
|
|
84
|
+
const res = createResponseMock();
|
|
85
|
+
expect(res._headers['key']).to.be.eq(undefined);
|
|
86
|
+
const ret = res.setHeader('key', ['foo', 'bar']);
|
|
87
|
+
expect(ret).to.be.eq(res);
|
|
88
|
+
expect(res._headers['key']).to.be.eql(['foo', 'bar']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should stringify an array elements', function () {
|
|
92
|
+
const res = createResponseMock();
|
|
93
|
+
expect(res._headers['key']).to.be.eq(undefined);
|
|
94
|
+
const ret = res.setHeader('key', [1, 2]);
|
|
95
|
+
expect(ret).to.be.eq(res);
|
|
96
|
+
expect(res._headers['key']).to.be.eql(['1', '2']);
|
|
97
|
+
});
|
|
82
98
|
});
|
|
83
99
|
|
|
84
100
|
describe('getHeader', function () {
|