@opra/core 1.0.0-alpha.23 → 1.0.0-alpha.24
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/constants.js +1 -2
- package/cjs/execution-context.js +1 -0
- package/cjs/http/express-adapter.js +15 -18
- package/cjs/http/http-adapter.js +2 -5
- package/cjs/http/http-context.js +14 -10
- package/cjs/http/{impl/http-handler.js → http-handler.js} +80 -93
- package/cjs/http/impl/multipart-reader.js +130 -37
- package/cjs/index.js +1 -1
- package/cjs/platform-adapter.js +0 -3
- package/esm/constants.js +0 -1
- package/esm/execution-context.js +1 -0
- package/esm/http/express-adapter.js +15 -18
- package/esm/http/http-adapter.js +2 -5
- package/esm/http/http-context.js +14 -10
- package/esm/http/{impl/http-handler.js → http-handler.js} +69 -82
- package/esm/http/impl/multipart-reader.js +132 -39
- package/esm/index.js +1 -1
- package/esm/platform-adapter.js +0 -3
- package/package.json +5 -5
- package/types/constants.d.ts +0 -1
- package/types/execution-context.d.ts +2 -1
- package/types/http/http-adapter.d.ts +23 -5
- package/types/http/{impl/http-handler.d.ts → http-handler.d.ts} +8 -8
- package/types/http/impl/multipart-reader.d.ts +38 -19
- package/types/index.d.ts +1 -1
- package/types/platform-adapter.d.ts +0 -4
- package/cjs/helpers/logger.js +0 -35
- package/esm/helpers/logger.js +0 -31
- package/i18n/i18n/en/error.json +0 -21
- package/types/helpers/logger.d.ts +0 -14
|
@@ -2,58 +2,147 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MultipartReader = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
|
|
7
|
+
const node_os_1 = tslib_1.__importDefault(require("node:os"));
|
|
8
|
+
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
9
|
+
const type_is_1 = tslib_1.__importDefault(require("@browsery/type-is"));
|
|
10
|
+
const common_1 = require("@opra/common");
|
|
11
|
+
const busboy_1 = tslib_1.__importDefault(require("busboy"));
|
|
5
12
|
const events_1 = require("events");
|
|
6
|
-
const formidable_1 = tslib_1.__importDefault(require("formidable"));
|
|
7
13
|
const promises_1 = tslib_1.__importDefault(require("fs/promises"));
|
|
14
|
+
const valgen_1 = require("valgen");
|
|
8
15
|
class MultipartReader extends events_1.EventEmitter {
|
|
9
|
-
constructor(
|
|
16
|
+
constructor(context, options, mediaType) {
|
|
10
17
|
super();
|
|
18
|
+
this.context = context;
|
|
19
|
+
this.mediaType = mediaType;
|
|
11
20
|
this._started = false;
|
|
21
|
+
this._finished = false;
|
|
12
22
|
this._cancelled = false;
|
|
13
23
|
this._items = [];
|
|
14
24
|
this._stack = [];
|
|
15
25
|
this.setMaxListeners(1000);
|
|
16
|
-
this.
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
form.once('error', () => {
|
|
26
|
+
this.tempDirectory = options?.tempDirectory || node_os_1.default.tmpdir();
|
|
27
|
+
const { request } = context;
|
|
28
|
+
const form = (0, busboy_1.default)({ headers: request.headers });
|
|
29
|
+
this._form = form;
|
|
30
|
+
form.once('error', (e) => {
|
|
22
31
|
this._cancelled = true;
|
|
32
|
+
this._finished = true;
|
|
23
33
|
if (this.listenerCount('error') > 0)
|
|
24
|
-
this.emit('error');
|
|
34
|
+
this.emit('error', e);
|
|
25
35
|
});
|
|
26
|
-
form.on('
|
|
27
|
-
|
|
36
|
+
form.on('close', () => {
|
|
37
|
+
this._finished = true;
|
|
38
|
+
});
|
|
39
|
+
form.on('field', (field, value, info) => {
|
|
40
|
+
const item = {
|
|
41
|
+
kind: 'field',
|
|
42
|
+
field,
|
|
43
|
+
value,
|
|
44
|
+
mimeType: info.mimeType,
|
|
45
|
+
encoding: info.encoding,
|
|
46
|
+
};
|
|
28
47
|
this._items.push(item);
|
|
29
48
|
this._stack.push(item);
|
|
30
49
|
this.emit('field', item);
|
|
31
50
|
this.emit('item', item);
|
|
32
51
|
});
|
|
33
|
-
form.on('file', (
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
form.on('file', (field, file, info) => {
|
|
53
|
+
const saveTo = node_path_1.default.join(this.tempDirectory, `opra-${generateFileName()}`);
|
|
54
|
+
file.pipe(node_fs_1.default.createWriteStream(saveTo));
|
|
55
|
+
file.once('end', () => {
|
|
56
|
+
const item = {
|
|
57
|
+
kind: 'file',
|
|
58
|
+
field,
|
|
59
|
+
storedPath: saveTo,
|
|
60
|
+
filename: info.filename,
|
|
61
|
+
mimeType: info.mimeType,
|
|
62
|
+
encoding: info.encoding,
|
|
63
|
+
};
|
|
64
|
+
this._items.push(item);
|
|
65
|
+
this._stack.push(item);
|
|
66
|
+
this.emit('file', item);
|
|
67
|
+
this.emit('item', item);
|
|
68
|
+
});
|
|
39
69
|
});
|
|
40
70
|
}
|
|
41
71
|
get items() {
|
|
42
72
|
return this._items;
|
|
43
73
|
}
|
|
44
|
-
getNext() {
|
|
45
|
-
|
|
74
|
+
async getNext() {
|
|
75
|
+
let item = this._stack.shift();
|
|
76
|
+
if (!item && !this._finished) {
|
|
46
77
|
this.resume();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
item = await new Promise((resolve, reject) => {
|
|
79
|
+
if (this._stack.length)
|
|
80
|
+
return resolve(this._stack.shift());
|
|
81
|
+
if (this._form.ended)
|
|
82
|
+
return resolve(undefined);
|
|
83
|
+
this._form.once('close', () => {
|
|
84
|
+
resolve(this._stack.shift());
|
|
85
|
+
});
|
|
86
|
+
this.once('item', () => {
|
|
87
|
+
this.pause();
|
|
88
|
+
resolve(this._stack.shift());
|
|
89
|
+
});
|
|
90
|
+
this.once('error', e => reject(e));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (item && this.mediaType) {
|
|
94
|
+
const field = this.mediaType.findMultipartField(item.field);
|
|
95
|
+
if (!field)
|
|
96
|
+
throw new common_1.BadRequestError(`Unknown multipart field (${item.field})`);
|
|
97
|
+
if (item.kind === 'field') {
|
|
98
|
+
const codec = field.generateCodec('decode');
|
|
99
|
+
item.value = codec(item.value);
|
|
100
|
+
}
|
|
101
|
+
else if (item.kind === 'file') {
|
|
102
|
+
if (field.contentType) {
|
|
103
|
+
const arr = Array.isArray(field.contentType) ? field.contentType : [field.contentType];
|
|
104
|
+
if (!(item.mimeType && arr.find(ct => type_is_1.default.is(item.mimeType, [ct])))) {
|
|
105
|
+
throw new common_1.BadRequestError(`Multipart field (${item.field}) do not accept this content type`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** if all items received we check for required items */
|
|
111
|
+
if (!item && this.mediaType && this.mediaType.multipartFields?.length > 0) {
|
|
112
|
+
const fieldsLeft = new Set(this.mediaType.multipartFields);
|
|
113
|
+
for (const x of this._items) {
|
|
114
|
+
const field = this.mediaType.findMultipartField(x.field);
|
|
115
|
+
if (field)
|
|
116
|
+
fieldsLeft.delete(field);
|
|
117
|
+
}
|
|
118
|
+
let issues;
|
|
119
|
+
for (const field of fieldsLeft) {
|
|
120
|
+
try {
|
|
121
|
+
(0, valgen_1.isNotNullish)(null, { onFail: () => `Multi part field "${String(field.fieldName)}" is required` });
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (!issues) {
|
|
125
|
+
issues = e.issues;
|
|
126
|
+
this.context.errors.push(e);
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
issues.push(...e.issues);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (this.context.errors.length)
|
|
133
|
+
throw this.context.errors[0];
|
|
134
|
+
}
|
|
135
|
+
return item;
|
|
136
|
+
}
|
|
137
|
+
async getAll() {
|
|
138
|
+
const items = [];
|
|
139
|
+
let item;
|
|
140
|
+
while (!this._cancelled && (item = await this.getNext())) {
|
|
141
|
+
items.push(item);
|
|
142
|
+
}
|
|
143
|
+
return items;
|
|
55
144
|
}
|
|
56
|
-
|
|
145
|
+
getAll_() {
|
|
57
146
|
if (this._form.ended)
|
|
58
147
|
return Promise.resolve([...this._items]);
|
|
59
148
|
this.resume();
|
|
@@ -70,21 +159,21 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
70
159
|
this.resume();
|
|
71
160
|
}
|
|
72
161
|
resume() {
|
|
73
|
-
if (!this._started)
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
162
|
+
if (!this._started) {
|
|
163
|
+
this._started = true;
|
|
164
|
+
this.context.request.pipe(this._form);
|
|
165
|
+
}
|
|
166
|
+
this.context.request.resume();
|
|
77
167
|
}
|
|
78
168
|
pause() {
|
|
79
|
-
|
|
80
|
-
this._form.pause();
|
|
169
|
+
this.context.request.pause();
|
|
81
170
|
}
|
|
82
|
-
async
|
|
171
|
+
async purge() {
|
|
83
172
|
const promises = [];
|
|
84
173
|
this._items.forEach(item => {
|
|
85
|
-
if (
|
|
174
|
+
if (item.kind !== 'file')
|
|
86
175
|
return;
|
|
87
|
-
const file = item.
|
|
176
|
+
const file = item.storedPath;
|
|
88
177
|
promises.push(new Promise(resolve => {
|
|
89
178
|
if (file._writeStream.closed)
|
|
90
179
|
return resolve();
|
|
@@ -97,3 +186,7 @@ class MultipartReader extends events_1.EventEmitter {
|
|
|
97
186
|
}
|
|
98
187
|
}
|
|
99
188
|
exports.MultipartReader = MultipartReader;
|
|
189
|
+
function generateFileName() {
|
|
190
|
+
const buf = Buffer.alloc(10);
|
|
191
|
+
return new Date().toISOString().substring(0, 10).replaceAll('-', '') + (0, node_crypto_1.randomFillSync)(buf).toString('hex');
|
|
192
|
+
}
|
package/cjs/index.js
CHANGED
|
@@ -10,11 +10,11 @@ const HttpOutgoingHost_ = tslib_1.__importStar(require("./http/impl/http-outgoin
|
|
|
10
10
|
const NodeIncomingMessageHost_ = tslib_1.__importStar(require("./http/impl/node-incoming-message.host.js"));
|
|
11
11
|
const NodeOutgoingMessageHost_ = tslib_1.__importStar(require("./http/impl/node-outgoing-message.host.js"));
|
|
12
12
|
tslib_1.__exportStar(require("./execution-context.js"), exports);
|
|
13
|
-
tslib_1.__exportStar(require("./helpers/logger.js"), exports);
|
|
14
13
|
tslib_1.__exportStar(require("./helpers/service-base.js"), exports);
|
|
15
14
|
tslib_1.__exportStar(require("./http/express-adapter.js"), exports);
|
|
16
15
|
tslib_1.__exportStar(require("./http/http-adapter.js"), exports);
|
|
17
16
|
tslib_1.__exportStar(require("./http/http-context.js"), exports);
|
|
17
|
+
tslib_1.__exportStar(require("./http/http-handler.js"), exports);
|
|
18
18
|
tslib_1.__exportStar(require("./http/impl/multipart-reader.js"), exports);
|
|
19
19
|
tslib_1.__exportStar(require("./http/interfaces/http-incoming.interface.js"), exports);
|
|
20
20
|
tslib_1.__exportStar(require("./http/interfaces/http-outgoing.interface.js"), exports);
|
package/cjs/platform-adapter.js
CHANGED
|
@@ -5,7 +5,6 @@ require("./augmentation/18n.augmentation.js");
|
|
|
5
5
|
const common_1 = require("@opra/common");
|
|
6
6
|
const strict_typed_events_1 = require("strict-typed-events");
|
|
7
7
|
const constants_js_1 = require("./constants.js");
|
|
8
|
-
const logger_js_1 = require("./helpers/logger.js");
|
|
9
8
|
const asset_cache_js_1 = require("./http/impl/asset-cache.js");
|
|
10
9
|
/**
|
|
11
10
|
* @class PlatformAdapter
|
|
@@ -15,8 +14,6 @@ class PlatformAdapter extends strict_typed_events_1.AsyncEventEmitter {
|
|
|
15
14
|
super();
|
|
16
15
|
this[constants_js_1.kAssetCache] = new asset_cache_js_1.AssetCache();
|
|
17
16
|
this.document = document;
|
|
18
|
-
this.logger =
|
|
19
|
-
options?.logger && options.logger instanceof logger_js_1.Logger ? options.logger : new logger_js_1.Logger({ instance: options?.logger });
|
|
20
17
|
this.i18n = options?.i18n || common_1.I18n.defaultInstance;
|
|
21
18
|
}
|
|
22
19
|
}
|
package/esm/constants.js
CHANGED
package/esm/execution-context.js
CHANGED
|
@@ -5,6 +5,7 @@ import { AsyncEventEmitter } from 'strict-typed-events';
|
|
|
5
5
|
export class ExecutionContext extends AsyncEventEmitter {
|
|
6
6
|
constructor(init) {
|
|
7
7
|
super();
|
|
8
|
+
this.errors = [];
|
|
8
9
|
this.document = init.document;
|
|
9
10
|
this.protocol = init.protocol;
|
|
10
11
|
this.platform = init.platform;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { HttpApi, NotFoundError } from '@opra/common';
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import * as nodePath from 'path';
|
|
4
|
-
import { kHandler } from '../constants.js';
|
|
5
4
|
import { HttpAdapter } from './http-adapter.js';
|
|
6
5
|
import { HttpContext } from './http-context.js';
|
|
7
6
|
import { HttpIncoming } from './interfaces/http-incoming.interface.js';
|
|
8
7
|
import { HttpOutgoing } from './interfaces/http-outgoing.interface.js';
|
|
8
|
+
import { wrapException } from './utils/wrap-exception';
|
|
9
9
|
export class ExpressAdapter extends HttpAdapter {
|
|
10
10
|
constructor(app, document, options) {
|
|
11
11
|
super(document, options);
|
|
@@ -36,7 +36,8 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
36
36
|
await resource.onShutdown.call(instance, resource);
|
|
37
37
|
}
|
|
38
38
|
catch (e) {
|
|
39
|
-
this.
|
|
39
|
+
if (this.listenerCount('error'))
|
|
40
|
+
this.emit('error', wrapException(e));
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -76,7 +77,7 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
76
77
|
/** Add an endpoint that returns document schema */
|
|
77
78
|
router.get('/\\$schema', (_req, _res, next) => {
|
|
78
79
|
const context = createContext(_req, _res);
|
|
79
|
-
this
|
|
80
|
+
this.handler.sendDocumentSchema(context).catch(next);
|
|
80
81
|
});
|
|
81
82
|
/** Add operation endpoints */
|
|
82
83
|
if (this.api.controllers.size) {
|
|
@@ -96,13 +97,13 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
96
97
|
operation,
|
|
97
98
|
operationHandler,
|
|
98
99
|
});
|
|
99
|
-
this
|
|
100
|
+
this.handler
|
|
100
101
|
.handleRequest(context)
|
|
101
102
|
.then(() => {
|
|
102
103
|
if (!_res.headersSent)
|
|
103
104
|
_next();
|
|
104
105
|
})
|
|
105
|
-
.catch((e) => this.
|
|
106
|
+
.catch((e) => this.emit('error', e));
|
|
106
107
|
});
|
|
107
108
|
}
|
|
108
109
|
if (controller.controllers.size) {
|
|
@@ -115,19 +116,15 @@ export class ExpressAdapter extends HttpAdapter {
|
|
|
115
116
|
}
|
|
116
117
|
/** Add an endpoint that returns 404 error at last */
|
|
117
118
|
router.use('*', (_req, _res, next) => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
}),
|
|
129
|
-
])
|
|
130
|
-
.catch(next);
|
|
119
|
+
const context = createContext(_req, _res);
|
|
120
|
+
context.errors.push(new NotFoundError({
|
|
121
|
+
message: `No endpoint found at [${_req.method}]${_req.baseUrl}`,
|
|
122
|
+
details: {
|
|
123
|
+
path: _req.baseUrl,
|
|
124
|
+
method: _req.method,
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
this.handler.sendResponse(context).catch(next);
|
|
131
128
|
});
|
|
132
129
|
}
|
|
133
130
|
_createControllers(controller) {
|
package/esm/http/http-adapter.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { HttpApi } from '@opra/common';
|
|
2
|
-
import { kHandler } from '../constants.js';
|
|
3
2
|
import { PlatformAdapter } from '../platform-adapter.js';
|
|
4
|
-
import { HttpHandler } from './
|
|
3
|
+
import { HttpHandler } from './http-handler.js';
|
|
5
4
|
/**
|
|
6
5
|
*
|
|
7
6
|
* @class HttpAdapter
|
|
@@ -12,10 +11,8 @@ export class HttpAdapter extends PlatformAdapter {
|
|
|
12
11
|
this.protocol = 'http';
|
|
13
12
|
if (!(document.api instanceof HttpApi))
|
|
14
13
|
throw new TypeError(`The document does not expose an HTTP Api`);
|
|
15
|
-
this
|
|
14
|
+
this.handler = new HttpHandler(this);
|
|
16
15
|
this.interceptors = [...(options?.interceptors || [])];
|
|
17
|
-
if (options?.onRequest)
|
|
18
|
-
this.on('request', options.onRequest);
|
|
19
16
|
}
|
|
20
17
|
get api() {
|
|
21
18
|
return this.document.api;
|
package/esm/http/http-context.js
CHANGED
|
@@ -25,6 +25,10 @@ export class HttpContext extends ExecutionContext {
|
|
|
25
25
|
this.pathParams = init.pathParams || {};
|
|
26
26
|
this.queryParams = init.queryParams || {};
|
|
27
27
|
this._body = init.body;
|
|
28
|
+
this.on('finish', () => {
|
|
29
|
+
if (this._multipartReader)
|
|
30
|
+
this._multipartReader.purge().catch(() => undefined);
|
|
31
|
+
});
|
|
28
32
|
}
|
|
29
33
|
get isMultipart() {
|
|
30
34
|
return !!this.request.is('multipart');
|
|
@@ -34,21 +38,21 @@ export class HttpContext extends ExecutionContext {
|
|
|
34
38
|
throw new InternalServerError('Request content is not a multipart content');
|
|
35
39
|
if (this._multipartReader)
|
|
36
40
|
return this._multipartReader;
|
|
37
|
-
const {
|
|
41
|
+
const { mediaType } = this;
|
|
38
42
|
if (mediaType?.contentType) {
|
|
39
43
|
const arr = Array.isArray(mediaType.contentType) ? mediaType.contentType : [mediaType.contentType];
|
|
40
44
|
const contentType = arr.find(ct => typeIs.is(ct, ['multipart']));
|
|
41
45
|
if (!contentType)
|
|
42
46
|
throw new NotAcceptableError('This endpoint does not accept multipart requests');
|
|
43
47
|
}
|
|
44
|
-
const reader = new MultipartReader(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
48
|
+
const reader = new MultipartReader(this, {
|
|
49
|
+
limits: {
|
|
50
|
+
fields: mediaType?.maxFields,
|
|
51
|
+
fieldSize: mediaType?.maxFieldsSize,
|
|
52
|
+
files: mediaType?.maxFiles,
|
|
53
|
+
fileSize: mediaType?.maxFileSize,
|
|
54
|
+
},
|
|
55
|
+
}, mediaType);
|
|
52
56
|
this._multipartReader = reader;
|
|
53
57
|
return reader;
|
|
54
58
|
}
|
|
@@ -66,7 +70,7 @@ export class HttpContext extends ExecutionContext {
|
|
|
66
70
|
if (mediaType && multipartFields?.length) {
|
|
67
71
|
const fieldsFound = new Map();
|
|
68
72
|
for (const item of parts) {
|
|
69
|
-
const field = mediaType.findMultipartField(item.
|
|
73
|
+
const field = mediaType.findMultipartField(item.field, item.kind);
|
|
70
74
|
if (field) {
|
|
71
75
|
fieldsFound.set(field, true);
|
|
72
76
|
this._body.push(item);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as process from 'node:process';
|
|
2
2
|
import typeIs from '@browsery/type-is';
|
|
3
|
-
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema,
|
|
3
|
+
import { BadRequestError, HttpHeaderCodes, HttpStatusCode, InternalServerError, isBlob, isReadableStream, IssueSeverity, MethodNotAllowedError, MimeTypes, OperationResult, OpraException, OpraSchema, } from '@opra/common';
|
|
4
4
|
import { parse as parseContentType } from 'content-type';
|
|
5
5
|
import { splitString } from 'fast-tokenizer';
|
|
6
6
|
import { asMutable } from 'ts-gems';
|
|
7
7
|
import { toArray, ValidationError, vg } from 'valgen';
|
|
8
|
-
import { kAssetCache } from '
|
|
9
|
-
import { wrapException } from '
|
|
8
|
+
import { kAssetCache } from '../constants';
|
|
9
|
+
import { wrapException } from './utils/wrap-exception';
|
|
10
10
|
/**
|
|
11
11
|
* @class HttpHandler
|
|
12
12
|
*/
|
|
@@ -38,7 +38,7 @@ export class HttpHandler {
|
|
|
38
38
|
throw e;
|
|
39
39
|
if (e instanceof ValidationError) {
|
|
40
40
|
throw new BadRequestError({
|
|
41
|
-
message:
|
|
41
|
+
message: 'Response validation failed',
|
|
42
42
|
code: 'RESPONSE_VALIDATION',
|
|
43
43
|
details: e.issues,
|
|
44
44
|
}, e);
|
|
@@ -64,19 +64,17 @@ export class HttpHandler {
|
|
|
64
64
|
let e = error;
|
|
65
65
|
if (e instanceof ValidationError) {
|
|
66
66
|
e = new InternalServerError({
|
|
67
|
-
message:
|
|
67
|
+
message: 'Response validation failed',
|
|
68
68
|
code: 'RESPONSE_VALIDATION',
|
|
69
69
|
details: e.issues,
|
|
70
70
|
}, e);
|
|
71
71
|
}
|
|
72
72
|
else
|
|
73
73
|
e = wrapException(e);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
response.end();
|
|
79
|
-
});
|
|
74
|
+
if (this.onError)
|
|
75
|
+
await this.onError(context, error);
|
|
76
|
+
context.errors.push(e);
|
|
77
|
+
await this.sendResponse(context);
|
|
80
78
|
}
|
|
81
79
|
finally {
|
|
82
80
|
await context.emitAsync('finish');
|
|
@@ -210,6 +208,7 @@ export class HttpHandler {
|
|
|
210
208
|
}
|
|
211
209
|
}
|
|
212
210
|
for (const prm of paramsLeft) {
|
|
211
|
+
key = String(prm.name);
|
|
213
212
|
// Throw error for required parameters
|
|
214
213
|
if (prm.required) {
|
|
215
214
|
const decode = getDecoder(prm);
|
|
@@ -261,7 +260,7 @@ export class HttpHandler {
|
|
|
261
260
|
const responseValue = await context.operationHandler.call(context.controllerInstance, context);
|
|
262
261
|
const { response } = context;
|
|
263
262
|
if (!response.writableEnded) {
|
|
264
|
-
await this.
|
|
263
|
+
await this.sendResponse(context, responseValue).finally(() => {
|
|
265
264
|
if (!response.writableEnded)
|
|
266
265
|
response.end();
|
|
267
266
|
});
|
|
@@ -273,7 +272,9 @@ export class HttpHandler {
|
|
|
273
272
|
* @param responseValue
|
|
274
273
|
* @protected
|
|
275
274
|
*/
|
|
276
|
-
async
|
|
275
|
+
async sendResponse(context, responseValue) {
|
|
276
|
+
if (context.errors.length)
|
|
277
|
+
return this.sendErrorResponse(context, context.errors);
|
|
277
278
|
const { response } = context;
|
|
278
279
|
const { document } = this.adapter;
|
|
279
280
|
const responseArgs = this._determineResponseArgs(context, responseValue);
|
|
@@ -364,6 +365,57 @@ export class HttpHandler {
|
|
|
364
365
|
x = String(body);
|
|
365
366
|
response.end(x);
|
|
366
367
|
}
|
|
368
|
+
async sendErrorResponse(context, errors) {
|
|
369
|
+
const { response } = context;
|
|
370
|
+
if (response.headersSent) {
|
|
371
|
+
response.end();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
errors = errors || context.errors;
|
|
375
|
+
const wrappedErrors = errors.map(wrapException);
|
|
376
|
+
if (!wrappedErrors.length)
|
|
377
|
+
wrappedErrors.push(new InternalServerError());
|
|
378
|
+
// Sort errors from fatal to info
|
|
379
|
+
wrappedErrors.sort((a, b) => {
|
|
380
|
+
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
381
|
+
if (i === 0)
|
|
382
|
+
return b.status - a.status;
|
|
383
|
+
return i;
|
|
384
|
+
});
|
|
385
|
+
context.errors = wrappedErrors;
|
|
386
|
+
let status = response.statusCode || 0;
|
|
387
|
+
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
388
|
+
status = wrappedErrors[0].status;
|
|
389
|
+
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
390
|
+
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
391
|
+
}
|
|
392
|
+
response.statusCode = status;
|
|
393
|
+
this.adapter.emitAsync('error', wrappedErrors[0], context).catch(() => undefined);
|
|
394
|
+
const { document } = this.adapter;
|
|
395
|
+
const dt = document.node.getComplexType('OperationResult');
|
|
396
|
+
let encode = this[kAssetCache].get(dt, 'encode');
|
|
397
|
+
if (!encode) {
|
|
398
|
+
encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
399
|
+
this[kAssetCache].set(dt, 'encode', encode);
|
|
400
|
+
}
|
|
401
|
+
const { i18n } = this.adapter;
|
|
402
|
+
const bodyObject = new OperationResult({
|
|
403
|
+
errors: wrappedErrors.map(x => {
|
|
404
|
+
const o = x.toJSON();
|
|
405
|
+
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
406
|
+
delete o.stack;
|
|
407
|
+
return i18n.deep(o);
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
const body = encode(bodyObject);
|
|
411
|
+
response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
|
|
412
|
+
response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
413
|
+
response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
|
414
|
+
response.setHeader(HttpHeaderCodes.Expires, '-1');
|
|
415
|
+
response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
416
|
+
response.send(JSON.stringify(body));
|
|
417
|
+
response.end();
|
|
418
|
+
}
|
|
367
419
|
/**
|
|
368
420
|
*
|
|
369
421
|
* @param context
|
|
@@ -498,11 +550,10 @@ export class HttpHandler {
|
|
|
498
550
|
const documentId = searchParams.get('id');
|
|
499
551
|
const doc = documentId ? document.findDocument(documentId) : document;
|
|
500
552
|
if (!doc) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
]);
|
|
553
|
+
context.errors.push(new BadRequestError({
|
|
554
|
+
message: `Document with given id [${documentId}] does not exists`,
|
|
555
|
+
}));
|
|
556
|
+
return this.sendResponse(context);
|
|
506
557
|
}
|
|
507
558
|
/** Check if response cache exists */
|
|
508
559
|
let responseBody = this[kAssetCache].get(doc, `$schema`);
|
|
@@ -514,68 +565,4 @@ export class HttpHandler {
|
|
|
514
565
|
}
|
|
515
566
|
response.end(responseBody);
|
|
516
567
|
}
|
|
517
|
-
async sendErrorResponse(response, errors) {
|
|
518
|
-
if (response.headersSent) {
|
|
519
|
-
response.end();
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (!errors.length)
|
|
523
|
-
errors.push(wrapException({ status: response.statusCode || 500 }));
|
|
524
|
-
const { logger } = this.adapter;
|
|
525
|
-
errors.forEach(x => {
|
|
526
|
-
if (x instanceof OpraException) {
|
|
527
|
-
switch (x.severity) {
|
|
528
|
-
case 'fatal':
|
|
529
|
-
logger.fatal(x);
|
|
530
|
-
break;
|
|
531
|
-
case 'warning':
|
|
532
|
-
logger.warn(x);
|
|
533
|
-
break;
|
|
534
|
-
default:
|
|
535
|
-
logger.error(x);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
else
|
|
539
|
-
logger.fatal(x);
|
|
540
|
-
});
|
|
541
|
-
const wrappedErrors = errors.map(wrapException);
|
|
542
|
-
// Sort errors from fatal to info
|
|
543
|
-
wrappedErrors.sort((a, b) => {
|
|
544
|
-
const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
|
|
545
|
-
if (i === 0)
|
|
546
|
-
return b.status - a.status;
|
|
547
|
-
return i;
|
|
548
|
-
});
|
|
549
|
-
let status = response.statusCode || 0;
|
|
550
|
-
if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
|
|
551
|
-
status = wrappedErrors[0].status;
|
|
552
|
-
if (status < Number(HttpStatusCode.BAD_REQUEST))
|
|
553
|
-
status = HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
554
|
-
}
|
|
555
|
-
response.statusCode = status;
|
|
556
|
-
const { document } = this.adapter;
|
|
557
|
-
const dt = document.node.getComplexType('OperationResult');
|
|
558
|
-
let encode = this[kAssetCache].get(dt, 'encode');
|
|
559
|
-
if (!encode) {
|
|
560
|
-
encode = dt.generateCodec('encode', { ignoreWriteonlyFields: true });
|
|
561
|
-
this[kAssetCache].set(dt, 'encode', encode);
|
|
562
|
-
}
|
|
563
|
-
const { i18n } = this.adapter;
|
|
564
|
-
const bodyObject = new OperationResult({
|
|
565
|
-
errors: wrappedErrors.map(x => {
|
|
566
|
-
const o = x.toJSON();
|
|
567
|
-
if (!(process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development'))
|
|
568
|
-
delete o.stack;
|
|
569
|
-
return i18n.deep(o);
|
|
570
|
-
}),
|
|
571
|
-
});
|
|
572
|
-
const body = encode(bodyObject);
|
|
573
|
-
response.setHeader(HttpHeaderCodes.Content_Type, MimeTypes.opra_response_json + '; charset=utf-8');
|
|
574
|
-
response.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
|
|
575
|
-
response.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
|
|
576
|
-
response.setHeader(HttpHeaderCodes.Expires, '-1');
|
|
577
|
-
response.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
|
|
578
|
-
response.send(JSON.stringify(body));
|
|
579
|
-
response.end();
|
|
580
|
-
}
|
|
581
568
|
}
|