@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 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
 
@@ -628,13 +628,30 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
628
628
  packageRoot: root,
629
629
  ErrorConstructor,
630
630
  });
631
- Object.assign(debugHeaders, (0, common_2.getHeadersFromMetadata)(trailingMetadata));
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
  }>;
@@ -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((_f = error === null || error === void 0 ? void 0 : error.response) === null || _f === void 0 ? void 0 : _f.data, ctx, ErrorConstructor), restStatus: responseStatus }), (0, redact_sensitive_headers_1.redactSensitiveHeaders)(parentCtx, headers), parentCtx, { debugHeaders: (0, common_1.sanitizeDebugHeaders)(debugHeaders) });
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
  }
@@ -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>>;
@@ -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[]>): Record<string, string>;
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;
@@ -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[`x-metadata-${key}`] = values
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/gateway",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",