@opra/http 1.4.2 → 1.4.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/cjs/http-context.js +8 -3
- package/cjs/http-handler.js +49 -16
- package/cjs/impl/multipart-reader.js +17 -6
- package/cjs/impl/node-incoming-message.host.js +9 -3
- package/cjs/impl/node-outgoing-message.host.js +9 -3
- package/cjs/interfaces/node-incoming-message.interface.js +2 -1
- package/cjs/type-guards.js +7 -2
- package/cjs/utils/body-reader.js +8 -3
- package/cjs/utils/convert-to-raw-headers.js +3 -1
- package/esm/express-adapter.js +2 -2
- package/esm/http-context.js +8 -3
- package/esm/http-handler.js +50 -17
- package/esm/impl/multipart-reader.js +17 -6
- package/esm/impl/node-incoming-message.host.js +10 -4
- package/esm/impl/node-outgoing-message.host.js +10 -4
- package/esm/interfaces/node-incoming-message.interface.js +3 -2
- package/esm/type-guards.js +7 -2
- package/esm/utils/body-reader.js +9 -4
- package/esm/utils/convert-to-raw-headers.js +3 -1
- package/package.json +3 -3
package/cjs/http-context.js
CHANGED
|
@@ -48,7 +48,9 @@ class HttpContext extends core_1.ExecutionContext {
|
|
|
48
48
|
return this._multipartReader;
|
|
49
49
|
const { mediaType } = this;
|
|
50
50
|
if (mediaType?.contentType) {
|
|
51
|
-
const arr = Array.isArray(mediaType.contentType)
|
|
51
|
+
const arr = Array.isArray(mediaType.contentType)
|
|
52
|
+
? mediaType.contentType
|
|
53
|
+
: [mediaType.contentType];
|
|
52
54
|
const contentType = arr.find(ct => type_is_1.default.is(ct, ['multipart']));
|
|
53
55
|
if (!contentType)
|
|
54
56
|
throw new common_1.NotAcceptableError('This endpoint does not accept multipart requests');
|
|
@@ -76,10 +78,13 @@ class HttpContext extends core_1.ExecutionContext {
|
|
|
76
78
|
this._body = [...parts];
|
|
77
79
|
return this._body;
|
|
78
80
|
}
|
|
79
|
-
this._body = await this.request.readBody({
|
|
81
|
+
this._body = await this.request.readBody({
|
|
82
|
+
limit: operation?.requestBody?.maxContentSize,
|
|
83
|
+
});
|
|
80
84
|
if (this._body != null) {
|
|
81
85
|
// Convert Buffer to string if media is text
|
|
82
|
-
if (Buffer.isBuffer(this._body) &&
|
|
86
|
+
if (Buffer.isBuffer(this._body) &&
|
|
87
|
+
request.is(['json', 'xml', 'txt', 'text'])) {
|
|
83
88
|
this._body = this._body.toString('utf-8');
|
|
84
89
|
}
|
|
85
90
|
// Transform text to Object if media is JSON
|
package/cjs/http-handler.js
CHANGED
|
@@ -127,12 +127,17 @@ class HttpHandler {
|
|
|
127
127
|
const getDecoder = (prm) => {
|
|
128
128
|
let decode = this[core_1.kAssetCache].get(prm, 'decode');
|
|
129
129
|
if (!decode) {
|
|
130
|
-
decode =
|
|
130
|
+
decode =
|
|
131
|
+
prm.type?.generateCodec('decode', { ignoreReadonlyFields: true }) ||
|
|
132
|
+
valgen_1.vg.isAny();
|
|
131
133
|
this[core_1.kAssetCache].set(prm, 'decode', decode);
|
|
132
134
|
}
|
|
133
135
|
return decode;
|
|
134
136
|
};
|
|
135
|
-
const paramsLeft = new Set([
|
|
137
|
+
const paramsLeft = new Set([
|
|
138
|
+
...operation.parameters,
|
|
139
|
+
...operation.owner.parameters,
|
|
140
|
+
]);
|
|
136
141
|
/** parse cookie parameters */
|
|
137
142
|
if (request.cookies) {
|
|
138
143
|
for (key of Object.keys(request.cookies)) {
|
|
@@ -146,7 +151,11 @@ class HttpHandler {
|
|
|
146
151
|
if (cntPrm)
|
|
147
152
|
paramsLeft.delete(cntPrm);
|
|
148
153
|
const decode = getDecoder(prm);
|
|
149
|
-
const v = decode(request.cookies[key], {
|
|
154
|
+
const v = decode(request.cookies[key], {
|
|
155
|
+
coerce: true,
|
|
156
|
+
label: key,
|
|
157
|
+
onFail,
|
|
158
|
+
});
|
|
150
159
|
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
151
160
|
if (v !== undefined)
|
|
152
161
|
context.cookies[prmName] = v;
|
|
@@ -165,7 +174,11 @@ class HttpHandler {
|
|
|
165
174
|
if (cntPrm)
|
|
166
175
|
paramsLeft.delete(cntPrm);
|
|
167
176
|
const decode = getDecoder(prm);
|
|
168
|
-
const v = decode(request.headers[key], {
|
|
177
|
+
const v = decode(request.headers[key], {
|
|
178
|
+
coerce: true,
|
|
179
|
+
label: key,
|
|
180
|
+
onFail,
|
|
181
|
+
});
|
|
169
182
|
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
170
183
|
if (v !== undefined)
|
|
171
184
|
context.headers[prmName] = v;
|
|
@@ -184,7 +197,11 @@ class HttpHandler {
|
|
|
184
197
|
if (cntPrm)
|
|
185
198
|
paramsLeft.delete(cntPrm);
|
|
186
199
|
const decode = getDecoder(prm);
|
|
187
|
-
const v = decode(request.params[key], {
|
|
200
|
+
const v = decode(request.params[key], {
|
|
201
|
+
coerce: true,
|
|
202
|
+
label: key,
|
|
203
|
+
onFail,
|
|
204
|
+
});
|
|
188
205
|
if (v !== undefined)
|
|
189
206
|
context.pathParams[key] = v;
|
|
190
207
|
}
|
|
@@ -206,7 +223,9 @@ class HttpHandler {
|
|
|
206
223
|
let values = searchParams?.getAll(key);
|
|
207
224
|
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
208
225
|
if (values?.length && prm.isArray) {
|
|
209
|
-
values = values
|
|
226
|
+
values = values
|
|
227
|
+
.map(v => (0, fast_tokenizer_1.splitString)(v, { delimiters: prm.arraySeparator, quotes: true }))
|
|
228
|
+
.flat();
|
|
210
229
|
values = values.map(v => decode(v, { coerce: true, label: key, onFail }));
|
|
211
230
|
if (values.length)
|
|
212
231
|
context.queryParams[prmName] = values;
|
|
@@ -255,7 +274,9 @@ class HttpHandler {
|
|
|
255
274
|
type_is_1.default.is(contentType, Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType]));
|
|
256
275
|
}
|
|
257
276
|
if (!mediaType) {
|
|
258
|
-
const contentTypes = operation.requestBody.content
|
|
277
|
+
const contentTypes = operation.requestBody.content
|
|
278
|
+
.map(mc => mc.contentType)
|
|
279
|
+
.flat();
|
|
259
280
|
throw new common_1.BadRequestError(`Request body should be one of required content types (${contentTypes.join(', ')})`);
|
|
260
281
|
}
|
|
261
282
|
(0, ts_gems_1.asMutable)(context).mediaType = mediaType;
|
|
@@ -304,7 +325,8 @@ class HttpHandler {
|
|
|
304
325
|
}
|
|
305
326
|
/** Validate response */
|
|
306
327
|
if (operationResponse?.type) {
|
|
307
|
-
if (!(body == null &&
|
|
328
|
+
if (!(body == null &&
|
|
329
|
+
statusCode === common_1.HttpStatusCode.NO_CONTENT)) {
|
|
308
330
|
/** Generate encoder */
|
|
309
331
|
const projection = responseArgs.projection || '*';
|
|
310
332
|
const assetKey = (0, super_fast_md5_1.md5)(String(projection));
|
|
@@ -345,8 +367,11 @@ class HttpHandler {
|
|
|
345
367
|
}
|
|
346
368
|
if (body instanceof common_1.OperationResult &&
|
|
347
369
|
operationResponse.type &&
|
|
348
|
-
operationResponse.type !==
|
|
349
|
-
|
|
370
|
+
operationResponse.type !==
|
|
371
|
+
document.node.getDataType(common_1.OperationResult)) {
|
|
372
|
+
body.type = operationResponse.type.name
|
|
373
|
+
? operationResponse.type.name
|
|
374
|
+
: '#embedded';
|
|
350
375
|
}
|
|
351
376
|
}
|
|
352
377
|
}
|
|
@@ -436,7 +461,8 @@ class HttpHandler {
|
|
|
436
461
|
const bodyObject = new common_1.OperationResult({
|
|
437
462
|
errors: errors.map(x => {
|
|
438
463
|
const o = x.toJSON();
|
|
439
|
-
if (!(process.env.NODE_ENV === 'dev' ||
|
|
464
|
+
if (!(process.env.NODE_ENV === 'dev' ||
|
|
465
|
+
process.env.NODE_ENV === 'development'))
|
|
440
466
|
delete o.stack;
|
|
441
467
|
return o; // i18n.deep(o);
|
|
442
468
|
}),
|
|
@@ -483,9 +509,13 @@ class HttpHandler {
|
|
|
483
509
|
_determineResponseArgs(context, body) {
|
|
484
510
|
const { response, operation } = context;
|
|
485
511
|
const hasBody = body != null;
|
|
486
|
-
const statusCode = !hasBody && response.statusCode === common_1.HttpStatusCode.OK
|
|
512
|
+
const statusCode = !hasBody && response.statusCode === common_1.HttpStatusCode.OK
|
|
513
|
+
? common_1.HttpStatusCode.NO_CONTENT
|
|
514
|
+
: response.statusCode;
|
|
487
515
|
/** Parse content-type header */
|
|
488
|
-
const parsedContentType = hasBody && response.hasHeader('content-type')
|
|
516
|
+
const parsedContentType = hasBody && response.hasHeader('content-type')
|
|
517
|
+
? (0, content_type_1.parse)(response)
|
|
518
|
+
: undefined;
|
|
489
519
|
let contentType = parsedContentType?.type;
|
|
490
520
|
/** Estimate content type if not defined */
|
|
491
521
|
if (hasBody && !contentType) {
|
|
@@ -563,7 +593,8 @@ class HttpHandler {
|
|
|
563
593
|
payload: body,
|
|
564
594
|
});
|
|
565
595
|
}
|
|
566
|
-
if ((composition === 'Entity.Create' ||
|
|
596
|
+
if ((composition === 'Entity.Create' ||
|
|
597
|
+
composition === 'Entity.Update') &&
|
|
567
598
|
composition &&
|
|
568
599
|
body.affected == null) {
|
|
569
600
|
body.affected = 1;
|
|
@@ -592,7 +623,8 @@ class HttpHandler {
|
|
|
592
623
|
break;
|
|
593
624
|
}
|
|
594
625
|
}
|
|
595
|
-
if (responseArgs.contentType &&
|
|
626
|
+
if (responseArgs.contentType &&
|
|
627
|
+
responseArgs.contentType !== parsedContentType?.type) {
|
|
596
628
|
response.setHeader('content-type', responseArgs.contentType);
|
|
597
629
|
}
|
|
598
630
|
if (responseArgs.contentType &&
|
|
@@ -611,7 +643,8 @@ class HttpHandler {
|
|
|
611
643
|
wrappedErrors.push(new common_1.InternalServerError());
|
|
612
644
|
// Sort errors from fatal to info
|
|
613
645
|
wrappedErrors.sort((a, b) => {
|
|
614
|
-
const i = common_1.IssueSeverity.Keys.indexOf(a.severity) -
|
|
646
|
+
const i = common_1.IssueSeverity.Keys.indexOf(a.severity) -
|
|
647
|
+
common_1.IssueSeverity.Keys.indexOf(b.severity);
|
|
615
648
|
if (i === 0)
|
|
616
649
|
return b.status - a.status;
|
|
617
650
|
return i;
|
|
@@ -102,14 +102,20 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
102
102
|
if (!field)
|
|
103
103
|
throw new common_1.BadRequestError(`Unknown multipart field (${item.field})`);
|
|
104
104
|
if (item.kind === 'field') {
|
|
105
|
-
const decode = field.generateCodec('decode', {
|
|
105
|
+
const decode = field.generateCodec('decode', {
|
|
106
|
+
ignoreReadonlyFields: true,
|
|
107
|
+
projection: '*',
|
|
108
|
+
});
|
|
106
109
|
item.value = decode(item.value, {
|
|
107
|
-
onFail: issue => `Multipart field (${item.field}) validation failed: ` +
|
|
110
|
+
onFail: issue => `Multipart field (${item.field}) validation failed: ` +
|
|
111
|
+
issue.message,
|
|
108
112
|
});
|
|
109
113
|
}
|
|
110
114
|
else if (item.kind === 'file') {
|
|
111
115
|
if (field.contentType) {
|
|
112
|
-
const arr = Array.isArray(field.contentType)
|
|
116
|
+
const arr = Array.isArray(field.contentType)
|
|
117
|
+
? field.contentType
|
|
118
|
+
: [field.contentType];
|
|
113
119
|
if (!(item.mimeType && arr.find(ct => type_is_1.default.is(item.mimeType, [ct])))) {
|
|
114
120
|
throw new common_1.BadRequestError(`Multipart field (${item.field}) do not accept this content type`);
|
|
115
121
|
}
|
|
@@ -117,7 +123,9 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
/** if all items received we check for required items */
|
|
120
|
-
if (this._finished &&
|
|
126
|
+
if (this._finished &&
|
|
127
|
+
this.mediaType &&
|
|
128
|
+
this.mediaType.multipartFields?.length > 0) {
|
|
121
129
|
const fieldsLeft = new Set(this.mediaType.multipartFields);
|
|
122
130
|
for (const x of this._items) {
|
|
123
131
|
const field = this.mediaType.findMultipartField(x.field);
|
|
@@ -129,7 +137,9 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
129
137
|
if (!field.required)
|
|
130
138
|
continue;
|
|
131
139
|
try {
|
|
132
|
-
(0, valgen_1.isNotNullish)(null, {
|
|
140
|
+
(0, valgen_1.isNotNullish)(null, {
|
|
141
|
+
onFail: () => `Multi part field "${String(field.fieldName)}" is required`,
|
|
142
|
+
});
|
|
133
143
|
}
|
|
134
144
|
catch (e) {
|
|
135
145
|
if (!issues) {
|
|
@@ -192,5 +202,6 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
192
202
|
exports.MultipartReader = MultipartReader;
|
|
193
203
|
function generateFileName() {
|
|
194
204
|
const buf = Buffer.alloc(10);
|
|
195
|
-
return new Date().toISOString().substring(0, 10).replace(/-/g, '') +
|
|
205
|
+
return (new Date().toISOString().substring(0, 10).replace(/-/g, '') +
|
|
206
|
+
(0, node_crypto_1.randomFillSync)(buf).toString('hex'));
|
|
196
207
|
}
|
|
@@ -37,10 +37,14 @@ class NodeIncomingMessageHost extends stream_1.Duplex {
|
|
|
37
37
|
this.body = Buffer.from(JSON.stringify(init.body), 'utf-8');
|
|
38
38
|
}
|
|
39
39
|
if (init.headers) {
|
|
40
|
-
this.rawHeaders = Array.isArray(init.headers)
|
|
40
|
+
this.rawHeaders = Array.isArray(init.headers)
|
|
41
|
+
? init.headers
|
|
42
|
+
: (0, convert_to_raw_headers_js_1.convertToRawHeaders)(init.headers);
|
|
41
43
|
}
|
|
42
44
|
if (init.trailers) {
|
|
43
|
-
this.rawTrailers = Array.isArray(init.trailers)
|
|
45
|
+
this.rawTrailers = Array.isArray(init.trailers)
|
|
46
|
+
? init.trailers
|
|
47
|
+
: (0, convert_to_raw_headers_js_1.convertToRawHeaders)(init.trailers);
|
|
44
48
|
}
|
|
45
49
|
this.ip = init.ip || '';
|
|
46
50
|
this.ips = init.ips || (this.ip ? [this.ip] : []);
|
|
@@ -53,7 +57,9 @@ class NodeIncomingMessageHost extends stream_1.Duplex {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
get httpVersion() {
|
|
56
|
-
return this.httpVersionMajor
|
|
60
|
+
return this.httpVersionMajor
|
|
61
|
+
? this.httpVersionMajor + '.' + this.httpVersionMinor
|
|
62
|
+
: '';
|
|
57
63
|
}
|
|
58
64
|
get headers() {
|
|
59
65
|
if (!this[exports.kHeaders])
|
|
@@ -27,7 +27,9 @@ class NodeOutgoingMessageHost extends stream_1.Duplex {
|
|
|
27
27
|
this.sendDate = !!init?.sendDate;
|
|
28
28
|
this.strictContentLength = !!init?.strictContentLength;
|
|
29
29
|
if (init.headers)
|
|
30
|
-
this.setHeaders(Array.isArray(init.headers)
|
|
30
|
+
this.setHeaders(Array.isArray(init.headers)
|
|
31
|
+
? new Map([init.headers])
|
|
32
|
+
: init.headers);
|
|
31
33
|
this.body = init.body;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -67,7 +69,9 @@ class NodeOutgoingMessageHost extends stream_1.Duplex {
|
|
|
67
69
|
}
|
|
68
70
|
addTrailers(headers) {
|
|
69
71
|
if (headers && typeof headers === 'object') {
|
|
70
|
-
const entries = typeof headers.entries === 'function'
|
|
72
|
+
const entries = typeof headers.entries === 'function'
|
|
73
|
+
? headers.entries()
|
|
74
|
+
: Object.entries(headers);
|
|
71
75
|
let trailers = this[exports.kOutTrailers];
|
|
72
76
|
if (trailers == null)
|
|
73
77
|
this[exports.kOutTrailers] = trailers = { __proto__: null };
|
|
@@ -98,7 +102,9 @@ class NodeOutgoingMessageHost extends stream_1.Duplex {
|
|
|
98
102
|
if (this.headersSent)
|
|
99
103
|
throw new Error(`Cannot set headers after they are sent to the client`);
|
|
100
104
|
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
|
101
|
-
const entries = typeof headers.entries === 'function'
|
|
105
|
+
const entries = typeof headers.entries === 'function'
|
|
106
|
+
? headers.entries()
|
|
107
|
+
: Object.entries(headers);
|
|
102
108
|
for (const entry of entries) {
|
|
103
109
|
this.setHeader(entry[0], entry[1]);
|
|
104
110
|
}
|
|
@@ -17,7 +17,8 @@ var NodeIncomingMessage;
|
|
|
17
17
|
* @param iterable
|
|
18
18
|
*/
|
|
19
19
|
function from(iterable) {
|
|
20
|
-
if (typeof iterable === 'object' &&
|
|
20
|
+
if (typeof iterable === 'object' &&
|
|
21
|
+
!((0, objects_1.isIterable)(iterable) || (0, objects_1.isAsyncIterable)(iterable))) {
|
|
21
22
|
return new node_incoming_message_host_js_1.NodeIncomingMessageHost(iterable);
|
|
22
23
|
}
|
|
23
24
|
const msg = new node_incoming_message_host_js_1.NodeIncomingMessageHost();
|
package/cjs/type-guards.js
CHANGED
|
@@ -6,7 +6,10 @@ exports.isNodeOutgoingMessage = isNodeOutgoingMessage;
|
|
|
6
6
|
exports.isHttpOutgoing = isHttpOutgoing;
|
|
7
7
|
const common_1 = require("@opra/common");
|
|
8
8
|
function isNodeIncomingMessage(v) {
|
|
9
|
-
return
|
|
9
|
+
return (v &&
|
|
10
|
+
typeof v.method === 'string' &&
|
|
11
|
+
Array.isArray(v.rawHeaders) &&
|
|
12
|
+
(0, common_1.isReadable)(v));
|
|
10
13
|
}
|
|
11
14
|
function isHttpIncoming(v) {
|
|
12
15
|
return (isNodeIncomingMessage(v) &&
|
|
@@ -18,5 +21,7 @@ function isNodeOutgoingMessage(v) {
|
|
|
18
21
|
return v && typeof v.getHeaders === 'function' && (0, common_1.isStream)(v);
|
|
19
22
|
}
|
|
20
23
|
function isHttpOutgoing(v) {
|
|
21
|
-
return isNodeOutgoingMessage(v) &&
|
|
24
|
+
return (isNodeOutgoingMessage(v) &&
|
|
25
|
+
typeof v.clearCookie === 'function' &&
|
|
26
|
+
typeof v.cookie === 'function');
|
|
22
27
|
}
|
package/cjs/utils/body-reader.js
CHANGED
|
@@ -62,13 +62,16 @@ class BodyReader extends events_1.EventEmitter {
|
|
|
62
62
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
|
|
63
63
|
*/
|
|
64
64
|
const contentLength = parseInt(this.req.headers['content-length'] || '0', 10);
|
|
65
|
-
if (this.req.headers['transfer-encoding'] === undefined &&
|
|
65
|
+
if (this.req.headers['transfer-encoding'] === undefined &&
|
|
66
|
+
!(contentLength && !isNaN(contentLength))) {
|
|
66
67
|
return this.onEnd();
|
|
67
68
|
}
|
|
68
69
|
// check the length and limit options.
|
|
69
70
|
// note: we intentionally leave the stream paused,
|
|
70
71
|
// so users should handle the stream themselves.
|
|
71
|
-
if (this.limit != null &&
|
|
72
|
+
if (this.limit != null &&
|
|
73
|
+
contentLength != null &&
|
|
74
|
+
contentLength > this.limit) {
|
|
72
75
|
return this.onEnd(new common_1.OpraHttpError({
|
|
73
76
|
message: 'Content Too Large',
|
|
74
77
|
code: 'HTTP.CONTENT_TOO_LARGE',
|
|
@@ -171,7 +174,9 @@ class BodyReader extends events_1.EventEmitter {
|
|
|
171
174
|
}
|
|
172
175
|
static encoderPipeline(req) {
|
|
173
176
|
const contentEncoding = req.headers['content-encoding'] || 'identity';
|
|
174
|
-
const contentEncodings = (Array.isArray(contentEncoding)
|
|
177
|
+
const contentEncodings = (Array.isArray(contentEncoding)
|
|
178
|
+
? contentEncoding
|
|
179
|
+
: contentEncoding.split(/\s*,\s*/))
|
|
175
180
|
.map(s => s.toLowerCase())
|
|
176
181
|
.reverse();
|
|
177
182
|
return contentEncodings.reduce((prev, encoding) => {
|
|
@@ -13,7 +13,9 @@ function convertToRawHeaders(src) {
|
|
|
13
13
|
return a;
|
|
14
14
|
}
|
|
15
15
|
if (flag === match_known_fields_js_1.COMMA_DELIMITED_FIELD || flag === match_known_fields_js_1.SEMICOLON_DELIMITED_FIELD) {
|
|
16
|
-
v = Array.isArray(v)
|
|
16
|
+
v = Array.isArray(v)
|
|
17
|
+
? v.join(flag === match_known_fields_js_1.COMMA_DELIMITED_FIELD ? ', ' : '; ')
|
|
18
|
+
: String(v);
|
|
17
19
|
}
|
|
18
20
|
else
|
|
19
21
|
v = Array.isArray(v) ? String(v[0]) : String(v);
|
package/esm/express-adapter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as nodePath from 'node:path';
|
|
2
|
-
import { HttpApi, NotFoundError } from '@opra/common';
|
|
3
|
-
import { Router } from 'express';
|
|
2
|
+
import { HttpApi, NotFoundError, } from '@opra/common';
|
|
3
|
+
import { Router, } from 'express';
|
|
4
4
|
import { HttpAdapter } from './http-adapter.js';
|
|
5
5
|
import { HttpContext } from './http-context.js';
|
|
6
6
|
import { HttpIncoming } from './interfaces/http-incoming.interface.js';
|
package/esm/http-context.js
CHANGED
|
@@ -44,7 +44,9 @@ export class HttpContext extends ExecutionContext {
|
|
|
44
44
|
return this._multipartReader;
|
|
45
45
|
const { mediaType } = this;
|
|
46
46
|
if (mediaType?.contentType) {
|
|
47
|
-
const arr = Array.isArray(mediaType.contentType)
|
|
47
|
+
const arr = Array.isArray(mediaType.contentType)
|
|
48
|
+
? mediaType.contentType
|
|
49
|
+
: [mediaType.contentType];
|
|
48
50
|
const contentType = arr.find(ct => typeIs.is(ct, ['multipart']));
|
|
49
51
|
if (!contentType)
|
|
50
52
|
throw new NotAcceptableError('This endpoint does not accept multipart requests');
|
|
@@ -72,10 +74,13 @@ export class HttpContext extends ExecutionContext {
|
|
|
72
74
|
this._body = [...parts];
|
|
73
75
|
return this._body;
|
|
74
76
|
}
|
|
75
|
-
this._body = await this.request.readBody({
|
|
77
|
+
this._body = await this.request.readBody({
|
|
78
|
+
limit: operation?.requestBody?.maxContentSize,
|
|
79
|
+
});
|
|
76
80
|
if (this._body != null) {
|
|
77
81
|
// Convert Buffer to string if media is text
|
|
78
|
-
if (Buffer.isBuffer(this._body) &&
|
|
82
|
+
if (Buffer.isBuffer(this._body) &&
|
|
83
|
+
request.is(['json', 'xml', 'txt', 'text'])) {
|
|
79
84
|
this._body = this._body.toString('utf-8');
|
|
80
85
|
}
|
|
81
86
|
// Transform text to Object if media is JSON
|
package/esm/http-handler.js
CHANGED
|
@@ -6,7 +6,7 @@ import { parse as parseContentType } from 'content-type';
|
|
|
6
6
|
import { splitString } from 'fast-tokenizer';
|
|
7
7
|
import { md5 } from 'super-fast-md5';
|
|
8
8
|
import { asMutable } from 'ts-gems';
|
|
9
|
-
import { toArray, ValidationError, vg } from 'valgen';
|
|
9
|
+
import { toArray, ValidationError, vg, } from 'valgen';
|
|
10
10
|
import { wrapException } from './utils/wrap-exception.js';
|
|
11
11
|
/**
|
|
12
12
|
* @class HttpHandler
|
|
@@ -123,12 +123,17 @@ export class HttpHandler {
|
|
|
123
123
|
const getDecoder = (prm) => {
|
|
124
124
|
let decode = this[kAssetCache].get(prm, 'decode');
|
|
125
125
|
if (!decode) {
|
|
126
|
-
decode =
|
|
126
|
+
decode =
|
|
127
|
+
prm.type?.generateCodec('decode', { ignoreReadonlyFields: true }) ||
|
|
128
|
+
vg.isAny();
|
|
127
129
|
this[kAssetCache].set(prm, 'decode', decode);
|
|
128
130
|
}
|
|
129
131
|
return decode;
|
|
130
132
|
};
|
|
131
|
-
const paramsLeft = new Set([
|
|
133
|
+
const paramsLeft = new Set([
|
|
134
|
+
...operation.parameters,
|
|
135
|
+
...operation.owner.parameters,
|
|
136
|
+
]);
|
|
132
137
|
/** parse cookie parameters */
|
|
133
138
|
if (request.cookies) {
|
|
134
139
|
for (key of Object.keys(request.cookies)) {
|
|
@@ -142,7 +147,11 @@ export class HttpHandler {
|
|
|
142
147
|
if (cntPrm)
|
|
143
148
|
paramsLeft.delete(cntPrm);
|
|
144
149
|
const decode = getDecoder(prm);
|
|
145
|
-
const v = decode(request.cookies[key], {
|
|
150
|
+
const v = decode(request.cookies[key], {
|
|
151
|
+
coerce: true,
|
|
152
|
+
label: key,
|
|
153
|
+
onFail,
|
|
154
|
+
});
|
|
146
155
|
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
147
156
|
if (v !== undefined)
|
|
148
157
|
context.cookies[prmName] = v;
|
|
@@ -161,7 +170,11 @@ export class HttpHandler {
|
|
|
161
170
|
if (cntPrm)
|
|
162
171
|
paramsLeft.delete(cntPrm);
|
|
163
172
|
const decode = getDecoder(prm);
|
|
164
|
-
const v = decode(request.headers[key], {
|
|
173
|
+
const v = decode(request.headers[key], {
|
|
174
|
+
coerce: true,
|
|
175
|
+
label: key,
|
|
176
|
+
onFail,
|
|
177
|
+
});
|
|
165
178
|
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
166
179
|
if (v !== undefined)
|
|
167
180
|
context.headers[prmName] = v;
|
|
@@ -180,7 +193,11 @@ export class HttpHandler {
|
|
|
180
193
|
if (cntPrm)
|
|
181
194
|
paramsLeft.delete(cntPrm);
|
|
182
195
|
const decode = getDecoder(prm);
|
|
183
|
-
const v = decode(request.params[key], {
|
|
196
|
+
const v = decode(request.params[key], {
|
|
197
|
+
coerce: true,
|
|
198
|
+
label: key,
|
|
199
|
+
onFail,
|
|
200
|
+
});
|
|
184
201
|
if (v !== undefined)
|
|
185
202
|
context.pathParams[key] = v;
|
|
186
203
|
}
|
|
@@ -202,7 +219,9 @@ export class HttpHandler {
|
|
|
202
219
|
let values = searchParams?.getAll(key);
|
|
203
220
|
const prmName = typeof prm.name === 'string' ? prm.name : key;
|
|
204
221
|
if (values?.length && prm.isArray) {
|
|
205
|
-
values = values
|
|
222
|
+
values = values
|
|
223
|
+
.map(v => splitString(v, { delimiters: prm.arraySeparator, quotes: true }))
|
|
224
|
+
.flat();
|
|
206
225
|
values = values.map(v => decode(v, { coerce: true, label: key, onFail }));
|
|
207
226
|
if (values.length)
|
|
208
227
|
context.queryParams[prmName] = values;
|
|
@@ -251,7 +270,9 @@ export class HttpHandler {
|
|
|
251
270
|
typeIs.is(contentType, Array.isArray(mc.contentType) ? mc.contentType : [mc.contentType]));
|
|
252
271
|
}
|
|
253
272
|
if (!mediaType) {
|
|
254
|
-
const contentTypes = operation.requestBody.content
|
|
273
|
+
const contentTypes = operation.requestBody.content
|
|
274
|
+
.map(mc => mc.contentType)
|
|
275
|
+
.flat();
|
|
255
276
|
throw new BadRequestError(`Request body should be one of required content types (${contentTypes.join(', ')})`);
|
|
256
277
|
}
|
|
257
278
|
asMutable(context).mediaType = mediaType;
|
|
@@ -300,7 +321,8 @@ export class HttpHandler {
|
|
|
300
321
|
}
|
|
301
322
|
/** Validate response */
|
|
302
323
|
if (operationResponse?.type) {
|
|
303
|
-
if (!(body == null &&
|
|
324
|
+
if (!(body == null &&
|
|
325
|
+
statusCode === HttpStatusCode.NO_CONTENT)) {
|
|
304
326
|
/** Generate encoder */
|
|
305
327
|
const projection = responseArgs.projection || '*';
|
|
306
328
|
const assetKey = md5(String(projection));
|
|
@@ -341,8 +363,11 @@ export class HttpHandler {
|
|
|
341
363
|
}
|
|
342
364
|
if (body instanceof OperationResult &&
|
|
343
365
|
operationResponse.type &&
|
|
344
|
-
operationResponse.type !==
|
|
345
|
-
|
|
366
|
+
operationResponse.type !==
|
|
367
|
+
document.node.getDataType(OperationResult)) {
|
|
368
|
+
body.type = operationResponse.type.name
|
|
369
|
+
? operationResponse.type.name
|
|
370
|
+
: '#embedded';
|
|
346
371
|
}
|
|
347
372
|
}
|
|
348
373
|
}
|
|
@@ -432,7 +457,8 @@ export class HttpHandler {
|
|
|
432
457
|
const bodyObject = new OperationResult({
|
|
433
458
|
errors: errors.map(x => {
|
|
434
459
|
const o = x.toJSON();
|
|
435
|
-
if (!(process.env.NODE_ENV === 'dev' ||
|
|
460
|
+
if (!(process.env.NODE_ENV === 'dev' ||
|
|
461
|
+
process.env.NODE_ENV === 'development'))
|
|
436
462
|
delete o.stack;
|
|
437
463
|
return o; // i18n.deep(o);
|
|
438
464
|
}),
|
|
@@ -479,9 +505,13 @@ export class HttpHandler {
|
|
|
479
505
|
_determineResponseArgs(context, body) {
|
|
480
506
|
const { response, operation } = context;
|
|
481
507
|
const hasBody = body != null;
|
|
482
|
-
const statusCode = !hasBody && response.statusCode === HttpStatusCode.OK
|
|
508
|
+
const statusCode = !hasBody && response.statusCode === HttpStatusCode.OK
|
|
509
|
+
? HttpStatusCode.NO_CONTENT
|
|
510
|
+
: response.statusCode;
|
|
483
511
|
/** Parse content-type header */
|
|
484
|
-
const parsedContentType = hasBody && response.hasHeader('content-type')
|
|
512
|
+
const parsedContentType = hasBody && response.hasHeader('content-type')
|
|
513
|
+
? parseContentType(response)
|
|
514
|
+
: undefined;
|
|
485
515
|
let contentType = parsedContentType?.type;
|
|
486
516
|
/** Estimate content type if not defined */
|
|
487
517
|
if (hasBody && !contentType) {
|
|
@@ -559,7 +589,8 @@ export class HttpHandler {
|
|
|
559
589
|
payload: body,
|
|
560
590
|
});
|
|
561
591
|
}
|
|
562
|
-
if ((composition === 'Entity.Create' ||
|
|
592
|
+
if ((composition === 'Entity.Create' ||
|
|
593
|
+
composition === 'Entity.Update') &&
|
|
563
594
|
composition &&
|
|
564
595
|
body.affected == null) {
|
|
565
596
|
body.affected = 1;
|
|
@@ -588,7 +619,8 @@ export class HttpHandler {
|
|
|
588
619
|
break;
|
|
589
620
|
}
|
|
590
621
|
}
|
|
591
|
-
if (responseArgs.contentType &&
|
|
622
|
+
if (responseArgs.contentType &&
|
|
623
|
+
responseArgs.contentType !== parsedContentType?.type) {
|
|
592
624
|
response.setHeader('content-type', responseArgs.contentType);
|
|
593
625
|
}
|
|
594
626
|
if (responseArgs.contentType &&
|
|
@@ -607,7 +639,8 @@ export class HttpHandler {
|
|
|
607
639
|
wrappedErrors.push(new InternalServerError());
|
|
608
640
|
// Sort errors from fatal to info
|
|
609
641
|
wrappedErrors.sort((a, b) => {
|
|
610
|
-
const i = IssueSeverity.Keys.indexOf(a.severity) -
|
|
642
|
+
const i = IssueSeverity.Keys.indexOf(a.severity) -
|
|
643
|
+
IssueSeverity.Keys.indexOf(b.severity);
|
|
611
644
|
if (i === 0)
|
|
612
645
|
return b.status - a.status;
|
|
613
646
|
return i;
|
|
@@ -98,14 +98,20 @@ export class MultipartReader extends EventEmitter {
|
|
|
98
98
|
if (!field)
|
|
99
99
|
throw new BadRequestError(`Unknown multipart field (${item.field})`);
|
|
100
100
|
if (item.kind === 'field') {
|
|
101
|
-
const decode = field.generateCodec('decode', {
|
|
101
|
+
const decode = field.generateCodec('decode', {
|
|
102
|
+
ignoreReadonlyFields: true,
|
|
103
|
+
projection: '*',
|
|
104
|
+
});
|
|
102
105
|
item.value = decode(item.value, {
|
|
103
|
-
onFail: issue => `Multipart field (${item.field}) validation failed: ` +
|
|
106
|
+
onFail: issue => `Multipart field (${item.field}) validation failed: ` +
|
|
107
|
+
issue.message,
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
110
|
else if (item.kind === 'file') {
|
|
107
111
|
if (field.contentType) {
|
|
108
|
-
const arr = Array.isArray(field.contentType)
|
|
112
|
+
const arr = Array.isArray(field.contentType)
|
|
113
|
+
? field.contentType
|
|
114
|
+
: [field.contentType];
|
|
109
115
|
if (!(item.mimeType && arr.find(ct => typeIs.is(item.mimeType, [ct])))) {
|
|
110
116
|
throw new BadRequestError(`Multipart field (${item.field}) do not accept this content type`);
|
|
111
117
|
}
|
|
@@ -113,7 +119,9 @@ export class MultipartReader extends EventEmitter {
|
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
/** if all items received we check for required items */
|
|
116
|
-
if (this._finished &&
|
|
122
|
+
if (this._finished &&
|
|
123
|
+
this.mediaType &&
|
|
124
|
+
this.mediaType.multipartFields?.length > 0) {
|
|
117
125
|
const fieldsLeft = new Set(this.mediaType.multipartFields);
|
|
118
126
|
for (const x of this._items) {
|
|
119
127
|
const field = this.mediaType.findMultipartField(x.field);
|
|
@@ -125,7 +133,9 @@ export class MultipartReader extends EventEmitter {
|
|
|
125
133
|
if (!field.required)
|
|
126
134
|
continue;
|
|
127
135
|
try {
|
|
128
|
-
isNotNullish(null, {
|
|
136
|
+
isNotNullish(null, {
|
|
137
|
+
onFail: () => `Multi part field "${String(field.fieldName)}" is required`,
|
|
138
|
+
});
|
|
129
139
|
}
|
|
130
140
|
catch (e) {
|
|
131
141
|
if (!issues) {
|
|
@@ -187,5 +197,6 @@ export class MultipartReader extends EventEmitter {
|
|
|
187
197
|
}
|
|
188
198
|
function generateFileName() {
|
|
189
199
|
const buf = Buffer.alloc(10);
|
|
190
|
-
return new Date().toISOString().substring(0, 10).replace(/-/g, '') +
|
|
200
|
+
return (new Date().toISOString().substring(0, 10).replace(/-/g, '') +
|
|
201
|
+
randomFillSync(buf).toString('hex'));
|
|
191
202
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAsyncIterable, isIterable } from '@jsopen/objects';
|
|
2
2
|
import { Duplex, Readable } from 'stream';
|
|
3
|
-
import { convertToHeaders, convertToHeadersDistinct } from '../utils/convert-to-headers.js';
|
|
3
|
+
import { convertToHeaders, convertToHeadersDistinct, } from '../utils/convert-to-headers.js';
|
|
4
4
|
import { convertToRawHeaders } from '../utils/convert-to-raw-headers.js';
|
|
5
5
|
export const CRLF = Buffer.from('\r\n');
|
|
6
6
|
export const kHeaders = Symbol.for('kHeaders');
|
|
@@ -34,10 +34,14 @@ export class NodeIncomingMessageHost extends Duplex {
|
|
|
34
34
|
this.body = Buffer.from(JSON.stringify(init.body), 'utf-8');
|
|
35
35
|
}
|
|
36
36
|
if (init.headers) {
|
|
37
|
-
this.rawHeaders = Array.isArray(init.headers)
|
|
37
|
+
this.rawHeaders = Array.isArray(init.headers)
|
|
38
|
+
? init.headers
|
|
39
|
+
: convertToRawHeaders(init.headers);
|
|
38
40
|
}
|
|
39
41
|
if (init.trailers) {
|
|
40
|
-
this.rawTrailers = Array.isArray(init.trailers)
|
|
42
|
+
this.rawTrailers = Array.isArray(init.trailers)
|
|
43
|
+
? init.trailers
|
|
44
|
+
: convertToRawHeaders(init.trailers);
|
|
41
45
|
}
|
|
42
46
|
this.ip = init.ip || '';
|
|
43
47
|
this.ips = init.ips || (this.ip ? [this.ip] : []);
|
|
@@ -50,7 +54,9 @@ export class NodeIncomingMessageHost extends Duplex {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
get httpVersion() {
|
|
53
|
-
return this.httpVersionMajor
|
|
57
|
+
return this.httpVersionMajor
|
|
58
|
+
? this.httpVersionMajor + '.' + this.httpVersionMinor
|
|
59
|
+
: '';
|
|
54
60
|
}
|
|
55
61
|
get headers() {
|
|
56
62
|
if (!this[kHeaders])
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
https://github.com/nodejs/
|
|
4
4
|
*/
|
|
5
5
|
import { Duplex } from 'stream';
|
|
6
|
-
import { validateHeaderName, validateHeaderValue, validateString } from '../utils/common.js';
|
|
6
|
+
import { validateHeaderName, validateHeaderValue, validateString, } from '../utils/common.js';
|
|
7
7
|
export const kOutHeaders = Symbol.for('kOutHeaders');
|
|
8
8
|
export const kOutTrailers = Symbol.for('kOutTrailers');
|
|
9
9
|
// noinspection JSUnusedLocalSymbols
|
|
@@ -24,7 +24,9 @@ export class NodeOutgoingMessageHost extends Duplex {
|
|
|
24
24
|
this.sendDate = !!init?.sendDate;
|
|
25
25
|
this.strictContentLength = !!init?.strictContentLength;
|
|
26
26
|
if (init.headers)
|
|
27
|
-
this.setHeaders(Array.isArray(init.headers)
|
|
27
|
+
this.setHeaders(Array.isArray(init.headers)
|
|
28
|
+
? new Map([init.headers])
|
|
29
|
+
: init.headers);
|
|
28
30
|
this.body = init.body;
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -64,7 +66,9 @@ export class NodeOutgoingMessageHost extends Duplex {
|
|
|
64
66
|
}
|
|
65
67
|
addTrailers(headers) {
|
|
66
68
|
if (headers && typeof headers === 'object') {
|
|
67
|
-
const entries = typeof headers.entries === 'function'
|
|
69
|
+
const entries = typeof headers.entries === 'function'
|
|
70
|
+
? headers.entries()
|
|
71
|
+
: Object.entries(headers);
|
|
68
72
|
let trailers = this[kOutTrailers];
|
|
69
73
|
if (trailers == null)
|
|
70
74
|
this[kOutTrailers] = trailers = { __proto__: null };
|
|
@@ -95,7 +99,9 @@ export class NodeOutgoingMessageHost extends Duplex {
|
|
|
95
99
|
if (this.headersSent)
|
|
96
100
|
throw new Error(`Cannot set headers after they are sent to the client`);
|
|
97
101
|
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
|
98
|
-
const entries = typeof headers.entries === 'function'
|
|
102
|
+
const entries = typeof headers.entries === 'function'
|
|
103
|
+
? headers.entries()
|
|
104
|
+
: Object.entries(headers);
|
|
99
105
|
for (const entry of entries) {
|
|
100
106
|
this.setHeader(entry[0], entry[1]);
|
|
101
107
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HTTPParser } from '@browsery/http-parser';
|
|
2
2
|
import { isAsyncIterable, isIterable } from '@jsopen/objects';
|
|
3
3
|
import { Readable } from 'stream';
|
|
4
|
-
import { CRLF, kHttpParser, NodeIncomingMessageHost } from '../impl/node-incoming-message.host.js';
|
|
4
|
+
import { CRLF, kHttpParser, NodeIncomingMessageHost, } from '../impl/node-incoming-message.host.js';
|
|
5
5
|
import { concatReadable } from '../utils/concat-readable.js';
|
|
6
6
|
/**
|
|
7
7
|
*
|
|
@@ -14,7 +14,8 @@ export var NodeIncomingMessage;
|
|
|
14
14
|
* @param iterable
|
|
15
15
|
*/
|
|
16
16
|
function from(iterable) {
|
|
17
|
-
if (typeof iterable === 'object' &&
|
|
17
|
+
if (typeof iterable === 'object' &&
|
|
18
|
+
!(isIterable(iterable) || isAsyncIterable(iterable))) {
|
|
18
19
|
return new NodeIncomingMessageHost(iterable);
|
|
19
20
|
}
|
|
20
21
|
const msg = new NodeIncomingMessageHost();
|
package/esm/type-guards.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isReadable, isStream } from '@opra/common';
|
|
2
2
|
export function isNodeIncomingMessage(v) {
|
|
3
|
-
return
|
|
3
|
+
return (v &&
|
|
4
|
+
typeof v.method === 'string' &&
|
|
5
|
+
Array.isArray(v.rawHeaders) &&
|
|
6
|
+
isReadable(v));
|
|
4
7
|
}
|
|
5
8
|
export function isHttpIncoming(v) {
|
|
6
9
|
return (isNodeIncomingMessage(v) &&
|
|
@@ -12,5 +15,7 @@ export function isNodeOutgoingMessage(v) {
|
|
|
12
15
|
return v && typeof v.getHeaders === 'function' && isStream(v);
|
|
13
16
|
}
|
|
14
17
|
export function isHttpOutgoing(v) {
|
|
15
|
-
return isNodeOutgoingMessage(v) &&
|
|
18
|
+
return (isNodeOutgoingMessage(v) &&
|
|
19
|
+
typeof v.clearCookie === 'function' &&
|
|
20
|
+
typeof v.cookie === 'function');
|
|
16
21
|
}
|
package/esm/utils/body-reader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import typeIs from '@browsery/type-is';
|
|
2
|
-
import { BadRequestError, InternalServerError, OpraHttpError } from '@opra/common';
|
|
2
|
+
import { BadRequestError, InternalServerError, OpraHttpError, } from '@opra/common';
|
|
3
3
|
import { Base64Decode } from 'base64-stream';
|
|
4
4
|
import byteParser from 'bytes';
|
|
5
5
|
import { parse as parseContentType } from 'content-type';
|
|
@@ -58,13 +58,16 @@ export class BodyReader extends EventEmitter {
|
|
|
58
58
|
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
|
|
59
59
|
*/
|
|
60
60
|
const contentLength = parseInt(this.req.headers['content-length'] || '0', 10);
|
|
61
|
-
if (this.req.headers['transfer-encoding'] === undefined &&
|
|
61
|
+
if (this.req.headers['transfer-encoding'] === undefined &&
|
|
62
|
+
!(contentLength && !isNaN(contentLength))) {
|
|
62
63
|
return this.onEnd();
|
|
63
64
|
}
|
|
64
65
|
// check the length and limit options.
|
|
65
66
|
// note: we intentionally leave the stream paused,
|
|
66
67
|
// so users should handle the stream themselves.
|
|
67
|
-
if (this.limit != null &&
|
|
68
|
+
if (this.limit != null &&
|
|
69
|
+
contentLength != null &&
|
|
70
|
+
contentLength > this.limit) {
|
|
68
71
|
return this.onEnd(new OpraHttpError({
|
|
69
72
|
message: 'Content Too Large',
|
|
70
73
|
code: 'HTTP.CONTENT_TOO_LARGE',
|
|
@@ -167,7 +170,9 @@ export class BodyReader extends EventEmitter {
|
|
|
167
170
|
}
|
|
168
171
|
static encoderPipeline(req) {
|
|
169
172
|
const contentEncoding = req.headers['content-encoding'] || 'identity';
|
|
170
|
-
const contentEncodings = (Array.isArray(contentEncoding)
|
|
173
|
+
const contentEncodings = (Array.isArray(contentEncoding)
|
|
174
|
+
? contentEncoding
|
|
175
|
+
: contentEncoding.split(/\s*,\s*/))
|
|
171
176
|
.map(s => s.toLowerCase())
|
|
172
177
|
.reverse();
|
|
173
178
|
return contentEncodings.reduce((prev, encoding) => {
|
|
@@ -10,7 +10,9 @@ export function convertToRawHeaders(src) {
|
|
|
10
10
|
return a;
|
|
11
11
|
}
|
|
12
12
|
if (flag === COMMA_DELIMITED_FIELD || flag === SEMICOLON_DELIMITED_FIELD) {
|
|
13
|
-
v = Array.isArray(v)
|
|
13
|
+
v = Array.isArray(v)
|
|
14
|
+
? v.join(flag === COMMA_DELIMITED_FIELD ? ', ' : '; ')
|
|
15
|
+
: String(v);
|
|
14
16
|
}
|
|
15
17
|
else
|
|
16
18
|
v = Array.isArray(v) ? String(v[0]) : String(v);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opra/http",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Opra Http Server Adapter",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"@browsery/http-parser": "^0.5.9-r1",
|
|
10
10
|
"@browsery/type-is": "^1.6.18-r5",
|
|
11
11
|
"@jsopen/objects": "^1.5.0",
|
|
12
|
-
"@opra/common": "^1.4.
|
|
13
|
-
"@opra/core": "^1.4.
|
|
12
|
+
"@opra/common": "^1.4.4",
|
|
13
|
+
"@opra/core": "^1.4.4",
|
|
14
14
|
"accepts": "^1.3.8",
|
|
15
15
|
"base64-stream": "^1.0.0",
|
|
16
16
|
"busboy": "^1.6.0",
|