@gravity-ui/gateway 4.4.0 → 4.6.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.
@@ -385,7 +385,7 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
385
385
  }
386
386
  return async function action(actionConfig) {
387
387
  var _a;
388
- const { args, requestId, headers, ctx: parentCtx, userId } = actionConfig;
388
+ const { args, requestId, headers, ctx: parentCtx, userId, abortSignal } = actionConfig;
389
389
  const { action } = config;
390
390
  const lang = headers[constants_1.DEFAULT_LANG_HEADER] || constants_1.Lang.Ru; // header might be empty string
391
391
  const ctx = parentCtx.create(`Gateway ${serviceName} ${actionName} [grpc]`, {
@@ -461,6 +461,7 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
461
461
  (0, common_2.handleError)(ErrorConstructor, error, ctx, 'getService failed');
462
462
  throw error;
463
463
  }
464
+ let stopListeningForAbort = null;
464
465
  // eslint-disable-next-line complexity
465
466
  return new Promise((resolve, reject) => {
466
467
  var _a, _b, _c, _d;
@@ -522,6 +523,12 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
522
523
  });
523
524
  const actionCall = service[action].bind(service);
524
525
  const stream = actionCall(body, serviceMetadata, serviceOptions);
526
+ stopListeningForAbort = (0, grpc_1.listenForAbort)({
527
+ signal: abortSignal,
528
+ config,
529
+ call: stream,
530
+ reject,
531
+ });
525
532
  stream.on('error', (error) => {
526
533
  ctx.log('ServerStream error', {
527
534
  debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
@@ -532,6 +539,7 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
532
539
  ctx.log('ServerStream status changed', status);
533
540
  });
534
541
  stream.on('end', () => {
542
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
535
543
  ctx.log('ServerStream request completed', {
536
544
  debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
537
545
  });
@@ -553,6 +561,15 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
553
561
  }
554
562
  const actionCall = service[action].bind(service);
555
563
  const stream = actionCall(serviceMetadata, serviceOptions, actionConfig.callback);
564
+ stopListeningForAbort = (0, grpc_1.listenForAbort)({
565
+ signal: abortSignal,
566
+ config,
567
+ call: stream,
568
+ reject,
569
+ });
570
+ stream.once('end', () => {
571
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
572
+ });
556
573
  resolve({ debugHeaders, stream });
557
574
  return;
558
575
  }
@@ -562,6 +579,12 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
562
579
  });
563
580
  const actionCall = service[action].bind(service);
564
581
  const stream = actionCall(serviceMetadata, serviceOptions);
582
+ stopListeningForAbort = (0, grpc_1.listenForAbort)({
583
+ signal: abortSignal,
584
+ config,
585
+ call: stream,
586
+ reject,
587
+ });
565
588
  stream.on('error', (error) => {
566
589
  ctx.log('BidiStream error', {
567
590
  debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
@@ -572,6 +595,7 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
572
595
  ctx.log('BidiStream status changed', status);
573
596
  });
574
597
  stream.on('end', () => {
598
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
575
599
  ctx.log('BidiStream request completed', {
576
600
  debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
577
601
  });
@@ -619,12 +643,14 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
619
643
  (0, common_2.handleError)(ErrorConstructor, error, ctx, 'getService failed');
620
644
  throw error;
621
645
  }
646
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
622
647
  // Update service
623
648
  actionCall = service[action].bind(service);
624
649
  callAction();
625
650
  return;
626
651
  }
627
652
  if (error) {
653
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
628
654
  reject(new parse_error_1.GrpcError('gRPC request error', (0, parse_error_1.parseGrpcError)(error, root, lang, config.decodeAnyMessageProtoLoaderOptions), error));
629
655
  return;
630
656
  }
@@ -660,11 +686,18 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
660
686
  debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
661
687
  });
662
688
  ctx.end();
689
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
663
690
  return resolve({ responseData, responseHeaders, debugHeaders });
664
691
  });
665
692
  call.on('status', (status) => {
666
693
  trailingMetadata = status.metadata.toJSON();
667
694
  });
695
+ stopListeningForAbort = (0, grpc_1.listenForAbort)({
696
+ signal: abortSignal,
697
+ config,
698
+ call,
699
+ reject,
700
+ });
668
701
  };
669
702
  callAction();
670
703
  }
@@ -672,6 +705,7 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
672
705
  }).catch((error) => {
673
706
  const grpcError = (0, parse_error_1.isGrpcError)(error) ? error : (0, parse_error_1.grpcErrorFactory)(error);
674
707
  processError(grpcError);
708
+ stopListeningForAbort === null || stopListeningForAbort === void 0 ? void 0 : stopListeningForAbort();
675
709
  return Promise.reject({ error: grpcError.getGatewayError(), debugHeaders });
676
710
  });
677
711
  };
@@ -28,7 +28,7 @@ function createMixedAction(config, api, serviceName, actionName, extra, ErrorCon
28
28
  });
29
29
  const contextApi = (0, create_context_api_1.generateContextApi)(api, Object.assign(Object.assign({}, context), { ctx }));
30
30
  try {
31
- const responseData = await config(contextApi, args, Object.assign({ headers: actionConfig.headers, lang: actionConfig.headers[constants_1.DEFAULT_LANG_HEADER] || constants_1.Lang.Ru, ctx }, extra));
31
+ const responseData = await config(contextApi, args, Object.assign(Object.assign({ headers: actionConfig.headers, lang: actionConfig.headers[constants_1.DEFAULT_LANG_HEADER] || constants_1.Lang.Ru, ctx }, extra), { abortSignal: actionConfig.abortSignal }));
32
32
  ctx.log('Request completed');
33
33
  return {
34
34
  responseData,
@@ -34,13 +34,13 @@ function getConfigSerializerFunction(config) {
34
34
  return undefined;
35
35
  }
36
36
  function createRestAction(endpoints, config, serviceKey, actionName, options, ErrorConstructor) {
37
- var _a, _b, _c;
37
+ var _a, _b, _c, _d;
38
38
  const timeout = (_c = (_a = config === null || config === void 0 ? void 0 : config.timeout) !== null && _a !== void 0 ? _a : (_b = options === null || options === void 0 ? void 0 : options.axiosConfig) === null || _b === void 0 ? void 0 : _b.timeout) !== null && _c !== void 0 ? _c : options === null || options === void 0 ? void 0 : options.timeout;
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.axiosRetryCondition, options === null || options === void 0 ? void 0 : options.axiosConfig, options === null || options === void 0 ? void 0 : options.axiosInterceptors);
39
+ const defaultAxiosClient = (0, axios_1.getAxiosClient)(timeout, config === null || config === void 0 ? void 0 : config.retries, (_d = config === null || config === void 0 ? void 0 : config.axiosRetryCondition) !== null && _d !== void 0 ? _d : options === null || options === void 0 ? void 0 : options.axiosRetryCondition, options === null || options === void 0 ? void 0 : options.axiosConfig, options === null || options === void 0 ? void 0 : options.axiosInterceptors);
40
40
  /* eslint-disable complexity */
41
41
  return async function action(actionConfig) {
42
42
  var _a, _b, _c, _d, _e, _f, _g, _h;
43
- const { args, requestId, headers: requestHeaders, ctx: parentCtx, authArgs, userId, } = actionConfig;
43
+ const { args, requestId, headers: requestHeaders, ctx: parentCtx, authArgs, userId, abortSignal, } = actionConfig;
44
44
  const debugHeaders = {};
45
45
  const lang = requestHeaders[constants_1.DEFAULT_LANG_HEADER] || constants_1.Lang.Ru; // header might be empty string
46
46
  const serviceName = (options === null || options === void 0 ? void 0 : options.serviceName) || serviceKey;
@@ -220,6 +220,7 @@ function createRestAction(endpoints, config, serviceKey, actionName, options, Er
220
220
  params: query,
221
221
  headers: ctx ? Object.assign(Object.assign({}, ctx.getMetadata()), headers) : headers,
222
222
  maxRedirects: config.maxRedirects,
223
+ signal: config.abortOnClientDisconnect ? abortSignal : undefined,
223
224
  };
224
225
  if (config.paramsSerializer) {
225
226
  Object.assign(requestConfig, {
package/build/index.js CHANGED
@@ -176,6 +176,11 @@ function generateGatewayApiController(schemasByScope, Api, config, controllerAct
176
176
  }
177
177
  const args = req.method === 'GET' ? req.query : req.body;
178
178
  try {
179
+ const abortController = new AbortController();
180
+ const handleCloseConnection = () => {
181
+ abortController.abort();
182
+ };
183
+ req.connection.once('close', handleCloseConnection);
179
184
  const apiAction = Api[scope][service][action];
180
185
  if (onBeforeAction) {
181
186
  const actionConfig = (_c = (_b = (_a = schemasByScope[scope]) === null || _a === void 0 ? void 0 : _a[service]) === null || _b === void 0 ? void 0 : _b.actions) === null || _c === void 0 ? void 0 : _c[action];
@@ -194,7 +199,9 @@ function generateGatewayApiController(schemasByScope, Api, config, controllerAct
194
199
  args,
195
200
  authArgs: config.getAuthArgs(req, res),
196
201
  userId,
202
+ abortSignal: abortController.signal,
197
203
  });
204
+ req.connection.removeListener('close', handleCloseConnection);
198
205
  if (withDebugHeaders) {
199
206
  res.set(debugHeaders);
200
207
  }
@@ -32,6 +32,7 @@ export interface ApiActionConfig<Context extends GatewayContext, TRequestData, T
32
32
  callback?: (response: TResponseData) => void;
33
33
  authArgs?: Record<string, unknown>;
34
34
  userId?: string;
35
+ abortSignal?: AbortSignal;
35
36
  }
36
37
  export interface GRPCActionData {
37
38
  [key: string]: unknown;
@@ -131,6 +132,7 @@ export interface ApiServiceBaseActionConfig<Context extends GatewayContext, TOut
131
132
  proxyHeaders?: ProxyHeaders;
132
133
  proxyResponseHeaders?: ProxyResponseHeaders;
133
134
  metadata?: Record<string, string | number | boolean>;
135
+ abortOnClientDisconnect?: boolean;
134
136
  }
135
137
  export interface ApiServiceRestActionConfig<Context extends GatewayContext, TOutput, TParams = undefined, TTransformed = TOutput> extends ApiServiceBaseActionConfig<Context, TOutput, TParams, TTransformed> {
136
138
  method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
@@ -139,6 +141,7 @@ export interface ApiServiceRestActionConfig<Context extends GatewayContext, TOut
139
141
  responseType?: AxiosRequestConfig['responseType'];
140
142
  expectedResponseContentType?: ResponseContentType | ResponseContentType[];
141
143
  maxRedirects?: number;
144
+ axiosRetryCondition?: AxiosRetryCondition;
142
145
  }
143
146
  export interface ApiServiceBaseGrpcActionConfig<Context extends GatewayContext, TOutput, TParams = undefined, TTransformed = TOutput> extends ApiServiceBaseActionConfig<Context, TOutput, TParams, TTransformed> {
144
147
  action: string;
@@ -167,6 +170,7 @@ export interface ApiServiceMixedExtra<Context extends GatewayContext, Req extend
167
170
  ctx: Context;
168
171
  config: GatewayConfig<Context, Req, Res>;
169
172
  grpcContext: GrpcContext;
173
+ abortSignal?: AbortSignal;
170
174
  }
171
175
  export type ApiServiceMixedActionConfig<Context extends GatewayContext, Req extends GatewayRequest<Context>, Res extends GatewayResponse, TOutput, TParams = undefined, TTransformed = TOutput> = (api: unknown, args: TParams, extra: ApiServiceMixedExtra<Context, Req, Res>) => Promise<TTransformed>;
172
176
  export type ApiServiceActionConfig<Context extends GatewayContext, Req extends GatewayRequest<Context>, Res extends GatewayResponse, TOutput, TParams = undefined, TTransformed = TOutput> = ApiServiceRestActionConfig<Context, TOutput, TParams, TTransformed> | ApiServiceGrpcActionConfig<Context, TOutput, TParams, TTransformed> | ApiServiceMixedActionConfig<Context, Req, Res, TOutput, TParams, TTransformed>;
@@ -1,5 +1,15 @@
1
1
  import * as grpc from '@grpc/grpc-js';
2
+ import { ClientDuplexStream, ClientReadableStream, ClientUnaryCall, ClientWritableStream } from '@grpc/grpc-js';
2
3
  import * as protobufjs from 'protobufjs';
3
4
  export declare function decodeAnyMessageRecursively(root: protobufjs.Root, message?: unknown, decodeAnyMessageProtoLoaderOptions?: protobufjs.IConversionOptions): unknown;
4
5
  export declare function isRetryableGrpcError(error?: grpc.ServiceError): boolean;
5
6
  export declare function isRecreateServiceError(error?: grpc.ServiceError): boolean;
7
+ export type ListenForAbortArgs = {
8
+ signal?: AbortSignal;
9
+ config: {
10
+ abortOnClientDisconnect?: boolean;
11
+ };
12
+ call: ClientUnaryCall | ClientReadableStream<unknown> | ClientWritableStream<unknown> | ClientDuplexStream<unknown, unknown>;
13
+ reject: (err: Error) => void;
14
+ };
15
+ export declare function listenForAbort({ signal, config, call, reject }: ListenForAbortArgs): () => void;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  /* eslint-disable camelcase */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.isRecreateServiceError = exports.isRetryableGrpcError = exports.decodeAnyMessageRecursively = void 0;
4
+ exports.listenForAbort = exports.isRecreateServiceError = exports.isRetryableGrpcError = exports.decodeAnyMessageRecursively = void 0;
5
5
  const constants_1 = require("../constants");
6
+ const parse_error_1 = require("./parse-error");
6
7
  function isEncodedMessage(message) {
7
8
  return Boolean(message.type_url && message.value);
8
9
  }
@@ -54,3 +55,25 @@ function isRecreateServiceError(error) {
54
55
  return constants_1.RECREATE_SERVICE_CODES.includes(error.code);
55
56
  }
56
57
  exports.isRecreateServiceError = isRecreateServiceError;
58
+ function listenForAbort({ signal, config, call, reject }) {
59
+ if (!signal || !config.abortOnClientDisconnect) {
60
+ return () => null;
61
+ }
62
+ const handleAbortSignal = () => {
63
+ call.cancel();
64
+ reject(new parse_error_1.GrpcError('Request was cancelled.', {
65
+ status: 499,
66
+ code: 'REQUEST_WAS_CANCELLED',
67
+ message: 'Request was cancelled because the original connection was disconnected.',
68
+ }));
69
+ };
70
+ if (signal.aborted) {
71
+ handleAbortSignal();
72
+ return () => null;
73
+ }
74
+ signal.addEventListener('abort', handleAbortSignal);
75
+ return () => {
76
+ signal.removeEventListener('abort', handleAbortSignal);
77
+ };
78
+ }
79
+ exports.listenForAbort = listenForAbort;
@@ -116,6 +116,10 @@ function parseRestError(error, lang) {
116
116
  status = 504;
117
117
  description = lang === constants_1.Lang.Ru ? 'Превышено время ожидания ответа' : 'Timeout exceeded';
118
118
  }
119
+ else if (code === 'ERR_CANCELED') {
120
+ status = 499;
121
+ description = lang === constants_1.Lang.Ru ? 'Запрос был отменен.' : 'Request was cancelled.';
122
+ }
119
123
  else {
120
124
  status = 500;
121
125
  description =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/gateway",
3
- "version": "4.4.0",
3
+ "version": "4.6.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",