@gravity-ui/gateway 2.0.0 → 2.2.0
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 +3 -1
- package/build/components/grpc.js +19 -2
- package/build/components/rest.d.ts +1 -0
- package/build/components/rest.js +41 -3
- package/build/index.js +4 -1
- package/build/models/common.d.ts +6 -0
- package/build/utils/common.d.ts +1 -1
- package/build/utils/common.js +2 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,10 +114,12 @@ interface GatewayConfig {
|
|
|
114
114
|
// Validation schema for parameters used when no schema is present in the action. Documentation: https://ajv.js.org/json-schema.html#json-data-type
|
|
115
115
|
// You can use DEFAULT_VALIDATION_SCHEMA from lib/constants.ts.
|
|
116
116
|
validationSchema?: object;
|
|
117
|
-
// Enables encoding of REST path arguments.
|
|
117
|
+
// Enables encoding of REST path arguments (default is true).
|
|
118
118
|
encodePathArgs?: boolean;
|
|
119
119
|
// Configuration for automatic connection re-establishment upon connection error through L3 load balancer (default is true).
|
|
120
120
|
grpcRecreateService?: boolean;
|
|
121
|
+
// Enable verification of response contentType header. Actual only for REST actions. This value can be set / redefined the in action confg.
|
|
122
|
+
expectedResponseContentType?: AxiosResponse['headers']['Content-Type'];
|
|
121
123
|
}
|
|
122
124
|
```
|
|
123
125
|
|
package/build/components/grpc.js
CHANGED
|
@@ -628,13 +628,30 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
|
|
|
628
628
|
packageRoot: root,
|
|
629
629
|
ErrorConstructor,
|
|
630
630
|
});
|
|
631
|
-
|
|
631
|
+
const responseHeaders = {};
|
|
632
|
+
if (config.proxyResponseHeaders) {
|
|
633
|
+
const proxyResponseHeaders = [];
|
|
634
|
+
const headersFromMetadata = (0, common_2.getHeadersFromMetadata)(trailingMetadata);
|
|
635
|
+
if (typeof config.proxyResponseHeaders === 'function') {
|
|
636
|
+
Object.assign(responseHeaders, config.proxyResponseHeaders(headersFromMetadata, 'grpc'));
|
|
637
|
+
}
|
|
638
|
+
else if (Array.isArray(config.proxyResponseHeaders)) {
|
|
639
|
+
proxyResponseHeaders.push(...config.proxyResponseHeaders);
|
|
640
|
+
}
|
|
641
|
+
for (const headerName of proxyResponseHeaders) {
|
|
642
|
+
if (responseHeaders[headerName] === undefined) {
|
|
643
|
+
responseHeaders[headerName] =
|
|
644
|
+
headersFromMetadata[headerName];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
Object.assign(debugHeaders, (0, common_2.getHeadersFromMetadata)(trailingMetadata, 'x-metadata-'));
|
|
632
649
|
sendStats(200, Object.assign(Object.assign({}, requestData), { responseSize: (0, object_sizeof_1.default)(response), grpcStatus: 0 }));
|
|
633
650
|
ctx.log('Request completed', {
|
|
634
651
|
debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
|
|
635
652
|
});
|
|
636
653
|
ctx.end();
|
|
637
|
-
return resolve({ responseData, debugHeaders });
|
|
654
|
+
return resolve({ responseData, responseHeaders, debugHeaders });
|
|
638
655
|
});
|
|
639
656
|
call.on('status', (status) => {
|
|
640
657
|
trailingMetadata = status.metadata.toJSON();
|
|
@@ -3,5 +3,6 @@ import { GatewayContext } from '../models/context';
|
|
|
3
3
|
import { AppErrorConstructor } from '../models/error';
|
|
4
4
|
export default function createRestAction<Context extends GatewayContext>(endpoints: EndpointsConfig | undefined, config: ApiServiceRestActionConfig<Context, any, any>, serviceKey: string, actionName: string, options: GatewayApiOptions<Context>, ErrorConstructor: AppErrorConstructor): (actionConfig: ApiActionConfig<Context, any>) => Promise<{
|
|
5
5
|
responseData: unknown;
|
|
6
|
+
responseHeaders?: Headers | undefined;
|
|
6
7
|
debugHeaders: Headers;
|
|
7
8
|
}>;
|
package/build/components/rest.js
CHANGED
|
@@ -39,7 +39,7 @@ function createRestAction(endpoints, config, serviceKey, actionName, options, Er
|
|
|
39
39
|
const defaultAxiosClient = (0, axios_1.getAxiosClient)(timeout, config === null || config === void 0 ? void 0 : config.retries, options === null || options === void 0 ? void 0 : options.axiosConfig);
|
|
40
40
|
/* eslint-disable complexity */
|
|
41
41
|
return async function action(actionConfig) {
|
|
42
|
-
var _a, _b, _c, _d, _e, _f;
|
|
42
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
43
43
|
const { args, requestId, headers: requestHeaders, ctx: parentCtx, authArgs } = actionConfig;
|
|
44
44
|
const debugHeaders = {};
|
|
45
45
|
const lang = requestHeaders[constants_1.DEFAULT_LANG_HEADER] || constants_1.Lang.Ru; // header might be empty string
|
|
@@ -220,8 +220,32 @@ function createRestAction(endpoints, config, serviceKey, actionName, options, Er
|
|
|
220
220
|
}
|
|
221
221
|
try {
|
|
222
222
|
const response = await axiosClient.request(requestConfig);
|
|
223
|
+
const responseHeaders = {};
|
|
223
224
|
const endRequestTime = Date.now();
|
|
224
225
|
requestData.requestTime = endRequestTime - startRequestTime;
|
|
226
|
+
const actualResponseContentType = (_f = response.headers) === null || _f === void 0 ? void 0 : _f['Content-Type'];
|
|
227
|
+
const expectedResponseContentType = config.expectedResponseContentType || options.expectedResponseContentType;
|
|
228
|
+
if (actualResponseContentType &&
|
|
229
|
+
expectedResponseContentType &&
|
|
230
|
+
actualResponseContentType !== expectedResponseContentType) {
|
|
231
|
+
ctx.log('Invalid response content type', {
|
|
232
|
+
expectedResponseContentType,
|
|
233
|
+
actualResponseContentType,
|
|
234
|
+
});
|
|
235
|
+
ctx.end();
|
|
236
|
+
return Promise.reject({
|
|
237
|
+
error: {
|
|
238
|
+
status: 415,
|
|
239
|
+
message: 'Response content type validation failed',
|
|
240
|
+
code: 'INVALID_RESPONSE_CONTENT_TYPE',
|
|
241
|
+
details: {
|
|
242
|
+
title: 'Invalid response content type',
|
|
243
|
+
description: `Expected to get ${expectedResponseContentType} but got ${actualResponseContentType}`,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
debugHeaders,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
225
249
|
if (config.transformResponseData) {
|
|
226
250
|
try {
|
|
227
251
|
response.data = await config.transformResponseData(response.data, {
|
|
@@ -235,6 +259,20 @@ function createRestAction(endpoints, config, serviceKey, actionName, options, Er
|
|
|
235
259
|
(0, common_1.handleError)(ErrorConstructor, error, ctx, 'Transform response data failed');
|
|
236
260
|
}
|
|
237
261
|
}
|
|
262
|
+
if (config.proxyResponseHeaders) {
|
|
263
|
+
const proxyResponseHeaders = [];
|
|
264
|
+
if (typeof config.proxyResponseHeaders === 'function') {
|
|
265
|
+
Object.assign(responseHeaders, config.proxyResponseHeaders(Object.assign({}, response.headers), 'rest'));
|
|
266
|
+
}
|
|
267
|
+
else if (Array.isArray(config.proxyResponseHeaders)) {
|
|
268
|
+
proxyResponseHeaders.push(...config.proxyResponseHeaders);
|
|
269
|
+
}
|
|
270
|
+
for (const headerName of proxyResponseHeaders) {
|
|
271
|
+
if (responseHeaders[headerName] === undefined) {
|
|
272
|
+
responseHeaders[headerName] = response.headers[headerName];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
238
276
|
if (options === null || options === void 0 ? void 0 : options.sendStats) {
|
|
239
277
|
options.sendStats(Object.assign(Object.assign({}, requestData), { responseSize: getRestResponseSize(response === null || response === void 0 ? void 0 : response.data, ctx, ErrorConstructor), restStatus: 200 }), (0, redact_sensitive_headers_1.redactSensitiveHeaders)(parentCtx, headers), parentCtx, { debugHeaders: (0, common_1.sanitizeDebugHeaders)(debugHeaders) });
|
|
240
278
|
}
|
|
@@ -243,7 +281,7 @@ function createRestAction(endpoints, config, serviceKey, actionName, options, Er
|
|
|
243
281
|
}
|
|
244
282
|
ctx.log('Request completed', { debugHeaders: (0, common_1.sanitizeDebugHeaders)(debugHeaders) });
|
|
245
283
|
ctx.end();
|
|
246
|
-
return { responseData: response.data, debugHeaders };
|
|
284
|
+
return { responseData: response.data, responseHeaders, debugHeaders };
|
|
247
285
|
}
|
|
248
286
|
catch (error) {
|
|
249
287
|
let parsedError;
|
|
@@ -274,7 +312,7 @@ function createRestAction(endpoints, config, serviceKey, actionName, options, Er
|
|
|
274
312
|
}
|
|
275
313
|
const responseStatus = lodash_1.default.get(parsedError, 'status') || lodash_1.default.get(error, 'status', 500);
|
|
276
314
|
if (options === null || options === void 0 ? void 0 : options.sendStats) {
|
|
277
|
-
options.sendStats(Object.assign(Object.assign({}, requestData), { responseSize: getRestResponseSize((
|
|
315
|
+
options.sendStats(Object.assign(Object.assign({}, requestData), { responseSize: getRestResponseSize((_g = error === null || error === void 0 ? void 0 : error.response) === null || _g === void 0 ? void 0 : _g.data, ctx, ErrorConstructor), restStatus: responseStatus }), (0, redact_sensitive_headers_1.redactSensitiveHeaders)(parentCtx, headers), parentCtx, { debugHeaders: (0, common_1.sanitizeDebugHeaders)(debugHeaders) });
|
|
278
316
|
}
|
|
279
317
|
else {
|
|
280
318
|
ctx.stats(Object.assign(Object.assign({}, requestData), { responseStatus }));
|
package/build/index.js
CHANGED
|
@@ -178,7 +178,7 @@ function generateGatewayApiController(schemasByScope, Api, config, controllerAct
|
|
|
178
178
|
throw { error, debugHeaders: {} };
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
-
const { responseData, debugHeaders } = await apiAction({
|
|
181
|
+
const { responseData, responseHeaders, debugHeaders } = await apiAction({
|
|
182
182
|
requestId: req.id,
|
|
183
183
|
headers: req.headers,
|
|
184
184
|
ctx: req.ctx,
|
|
@@ -188,6 +188,9 @@ function generateGatewayApiController(schemasByScope, Api, config, controllerAct
|
|
|
188
188
|
if (withDebugHeaders) {
|
|
189
189
|
res.set(debugHeaders);
|
|
190
190
|
}
|
|
191
|
+
if (responseHeaders) {
|
|
192
|
+
res.set(responseHeaders);
|
|
193
|
+
}
|
|
191
194
|
if (onRequestSuccess) {
|
|
192
195
|
return onRequestSuccess(req, res, responseData);
|
|
193
196
|
}
|
package/build/models/common.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface GatewayError {
|
|
|
62
62
|
}
|
|
63
63
|
export type ProxyHeadersFunction = (headers: IncomingHttpHeaders, type: ControllerType) => IncomingHttpHeaders;
|
|
64
64
|
export type ProxyHeaders = string[] | ProxyHeadersFunction;
|
|
65
|
+
export type ProxyResponseHeadersFunction = (headers: Headers, type: ControllerType) => Headers;
|
|
66
|
+
export type ProxyResponseHeaders = string[] | ProxyResponseHeadersFunction;
|
|
65
67
|
export type GetAuthHeadersParams<AuthArgs = Record<string, unknown>> = {
|
|
66
68
|
actionType: 'rest' | 'grpc';
|
|
67
69
|
serviceName: string;
|
|
@@ -79,6 +81,7 @@ export interface GatewayApiOptions<Context extends GatewayContext> {
|
|
|
79
81
|
proxyHeaders?: ProxyHeaders;
|
|
80
82
|
validationSchema?: object;
|
|
81
83
|
encodePathArgs?: boolean;
|
|
84
|
+
expectedResponseContentType?: AxiosResponse['headers']['Content-Type'];
|
|
82
85
|
getAuthHeaders: GetAuthHeaders;
|
|
83
86
|
}
|
|
84
87
|
export interface ParamsOutput {
|
|
@@ -114,6 +117,7 @@ export interface ApiServiceBaseActionConfig<Context extends GatewayContext, TOut
|
|
|
114
117
|
retries?: number;
|
|
115
118
|
idempotency?: boolean;
|
|
116
119
|
proxyHeaders?: ProxyHeaders;
|
|
120
|
+
proxyResponseHeaders?: ProxyResponseHeaders;
|
|
117
121
|
metadata?: Record<string, string | number | boolean>;
|
|
118
122
|
}
|
|
119
123
|
export interface ApiServiceRestActionConfig<Context extends GatewayContext, TOutput, TParams = undefined, TTransformed = TOutput> extends ApiServiceBaseActionConfig<Context, TOutput, TParams, TTransformed> {
|
|
@@ -121,6 +125,7 @@ export interface ApiServiceRestActionConfig<Context extends GatewayContext, TOut
|
|
|
121
125
|
path: (args: TParams) => string;
|
|
122
126
|
paramsSerializer?: AxiosRequestConfig['paramsSerializer'];
|
|
123
127
|
responseType?: AxiosRequestConfig['responseType'];
|
|
128
|
+
expectedResponseContentType?: AxiosResponse['headers']['Content-Type'];
|
|
124
129
|
maxRedirects?: number;
|
|
125
130
|
}
|
|
126
131
|
export interface ApiServiceBaseGrpcActionConfig<Context extends GatewayContext, TOutput, TParams = undefined, TTransformed = TOutput> extends ApiServiceBaseActionConfig<Context, TOutput, TParams, TTransformed> {
|
|
@@ -185,6 +190,7 @@ export interface GatewayActionHeaders {
|
|
|
185
190
|
}
|
|
186
191
|
export interface GatewayActionUnaryResponse<TAction> extends GatewayActionHeaders {
|
|
187
192
|
responseData: ApiActionResponseType<TAction>;
|
|
193
|
+
responseHeaders?: Headers;
|
|
188
194
|
}
|
|
189
195
|
export interface GatewayActionClientStreamResponse<TAction> extends GatewayActionHeaders {
|
|
190
196
|
stream: ClientWritableStream<ApiActionResponseType<TAction>>;
|
package/build/utils/common.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export declare function getKeys<T extends object>(obj: T): (keyof T)[];
|
|
|
11
11
|
* This function should only use to sanitize debugHeaders that are creating in our code
|
|
12
12
|
*/
|
|
13
13
|
export declare function sanitizeDebugHeaders(debugHeaders: Headers): _.Omit<Headers, "x-api-request-body">;
|
|
14
|
-
export declare function getHeadersFromMetadata(metadata: Record<string, grpc.MetadataValue[]
|
|
14
|
+
export declare function getHeadersFromMetadata(metadata: Record<string, grpc.MetadataValue[]>, prefix?: string): Record<string, string>;
|
|
15
15
|
export declare function handleError<Context extends GatewayContext>(ErrorConstructor: AppErrorConstructor, error: unknown, ctx: Context, message: string, extra?: Dict): void;
|
package/build/utils/common.js
CHANGED
|
@@ -30,11 +30,9 @@ function sanitizeDebugHeaders(debugHeaders) {
|
|
|
30
30
|
return lodash_1.default.omit(debugHeaders, ['x-api-request-body']);
|
|
31
31
|
}
|
|
32
32
|
exports.sanitizeDebugHeaders = sanitizeDebugHeaders;
|
|
33
|
-
function getHeadersFromMetadata(metadata) {
|
|
33
|
+
function getHeadersFromMetadata(metadata, prefix = '') {
|
|
34
34
|
return Object.entries(metadata).reduce((headers, [key, values]) => {
|
|
35
|
-
headers[
|
|
36
|
-
.filter((value) => typeof value === 'string')
|
|
37
|
-
.join(' ');
|
|
35
|
+
headers[`${prefix}${key}`] = values.filter((value) => typeof value === 'string').join(' ');
|
|
38
36
|
return headers;
|
|
39
37
|
}, {});
|
|
40
38
|
}
|