@gravity-ui/gateway 4.3.0 → 4.4.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 +359 -63
- package/build/components/grpc.js +6 -2
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/utils/grpc.d.ts +1 -1
- package/build/utils/grpc.js +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
# @gravity-ui/gateway · [](https://www.npmjs.com/package/@gravity-ui/gateway) [](https://github.com/gravity-ui/gateway/actions/workflows/ci.yml?query=branch:main)
|
|
2
2
|
|
|
3
|
-
Express controller for working with REST
|
|
4
|
-
|
|
5
|
-
##
|
|
3
|
+
A flexible and powerful Express controller for working with REST and gRPC APIs in Node.js applications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Basic Usage](#basic-usage)
|
|
9
|
+
- [Configuration](#configuration)
|
|
10
|
+
- [Config Structure](#config-structure)
|
|
11
|
+
- [Validation Schema](#validation-schema)
|
|
12
|
+
- [Using the API in Node.js](#using-the-api-in-nodejs)
|
|
13
|
+
- [Schema Scopes](#schema-scopes)
|
|
14
|
+
- [Connecting Specific Actions](#connecting-specific-actions)
|
|
15
|
+
- [Overriding Endpoints](#overriding-endpoints)
|
|
16
|
+
- [Authentication](#authentication)
|
|
17
|
+
- [Error Handling](#error-handling)
|
|
18
|
+
- [gRPC Reflection](#grpc-reflection-for-grpc-actions)
|
|
19
|
+
- [Retryable Errors](#retryable-errors)
|
|
20
|
+
- [Development](#development)
|
|
21
|
+
- [Running Tests](#running-tests)
|
|
22
|
+
- [Contributing](#contributing)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
6
25
|
|
|
7
26
|
```shell
|
|
8
|
-
npm install --save
|
|
27
|
+
npm install --save @gravity-ui/gateway
|
|
9
28
|
```
|
|
10
29
|
|
|
11
|
-
## Usage
|
|
30
|
+
## Basic Usage
|
|
12
31
|
|
|
13
|
-
First
|
|
32
|
+
First, create a controller by importing Gateway and your API schemas:
|
|
14
33
|
|
|
15
34
|
```javascript
|
|
16
35
|
import {getGatewayControllers} from '@gravity-ui/gateway';
|
|
@@ -27,7 +46,7 @@ const {controller: gatewayController} = getGatewayControllers({root: Schema}, co
|
|
|
27
46
|
export default gatewayController;
|
|
28
47
|
```
|
|
29
48
|
|
|
30
|
-
|
|
49
|
+
Then, connect the controller to your Express routes (using [expresskit](https://github.com/gravity-ui/expresskit)):
|
|
31
50
|
|
|
32
51
|
```javascript
|
|
33
52
|
{
|
|
@@ -37,11 +56,12 @@ Next, the controller described above should be connected to a route of the follo
|
|
|
37
56
|
|
|
38
57
|
The `prefix` can be any prefix for API endpoints (for example, `/gateway/:service/:action`).
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
## Configuration
|
|
41
60
|
|
|
42
61
|
```typescript
|
|
43
62
|
import {AxiosRequestConfig} from 'axios';
|
|
44
63
|
import {IncomingHttpHeaders} from 'http';
|
|
64
|
+
import {IAxiosRetryConfig} from 'axios-retry';
|
|
45
65
|
|
|
46
66
|
interface OnUnknownActionData {
|
|
47
67
|
service?: string;
|
|
@@ -67,6 +87,11 @@ type SendStats = (
|
|
|
67
87
|
meta: {debugHeaders: Headers},
|
|
68
88
|
) => void;
|
|
69
89
|
|
|
90
|
+
type GrpcRetryCondition = (error: ServiceError) => boolean;
|
|
91
|
+
type AxiosRetryCondition = IAxiosRetryConfig['retryCondition'];
|
|
92
|
+
|
|
93
|
+
type ControllerType = 'rest' | 'grpc';
|
|
94
|
+
|
|
70
95
|
type ProxyHeadersFunction = (
|
|
71
96
|
headers: IncomingHttpHeaders,
|
|
72
97
|
type: ControllerType,
|
|
@@ -74,19 +99,48 @@ type ProxyHeadersFunction = (
|
|
|
74
99
|
type ProxyHeaders = string[] | ProxyHeadersFunction;
|
|
75
100
|
type ResponseContentType = AxiosResponse['headers']['Content-Type'];
|
|
76
101
|
|
|
102
|
+
type GetAuthHeadersParams<AuthArgs = Record<string, unknown>> = {
|
|
103
|
+
actionType: ControllerType;
|
|
104
|
+
serviceName: string;
|
|
105
|
+
requestHeaders: Headers;
|
|
106
|
+
authArgs: AuthArgs | undefined;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
interface AppErrorArgs {
|
|
110
|
+
code?: string | number;
|
|
111
|
+
details?: object;
|
|
112
|
+
debug?: object;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface AppErrorWrapArgs extends AppErrorArgs {
|
|
116
|
+
message?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface AppErrorConstructor {
|
|
120
|
+
new (message?: string, args?: AppErrorArgs): Error;
|
|
121
|
+
|
|
122
|
+
wrap: (error: Error, args?: AppErrorWrapArgs) => Error;
|
|
123
|
+
}
|
|
124
|
+
|
|
77
125
|
interface GatewayConfig {
|
|
78
|
-
// Gateway Installation (external/internal/...). If
|
|
126
|
+
// Gateway Installation (external/internal/...). If not provided, determined from process.env.APP_INSTALLATION.
|
|
79
127
|
installation?: string;
|
|
80
|
-
|
|
128
|
+
|
|
129
|
+
// Gateway Environment (production/testing/...). If not provided, determined from process.env.APP_ENV.
|
|
81
130
|
env?: string;
|
|
131
|
+
|
|
82
132
|
// Additional gRPC client options.
|
|
83
133
|
grpcOptions?: object;
|
|
134
|
+
|
|
84
135
|
// Additional Axios client options.
|
|
85
136
|
axiosConfig?: AxiosRequestConfig;
|
|
86
|
-
|
|
137
|
+
|
|
138
|
+
// List of actions to connect from the schema. By default, all actions are connected.
|
|
87
139
|
actions?: string[];
|
|
140
|
+
|
|
88
141
|
// Called when an unknown service or action is provided.
|
|
89
142
|
onUnknownAction?: (req: Request, res: Response, data: OnUnknownActionData) => any;
|
|
143
|
+
|
|
90
144
|
// Called before the request is executed.
|
|
91
145
|
onBeforeAction?: (
|
|
92
146
|
req: Request,
|
|
@@ -96,42 +150,90 @@ interface GatewayConfig {
|
|
|
96
150
|
action: string,
|
|
97
151
|
config?: ApiServiceActionConfig,
|
|
98
152
|
) => any;
|
|
153
|
+
|
|
99
154
|
// Called upon successful completion of the request.
|
|
100
155
|
onRequestSuccess?: (req: Request, res: Response, data: any) => any;
|
|
156
|
+
|
|
101
157
|
// Called in case of unsuccessful request execution.
|
|
102
158
|
onRequestFailed?: (req: Request, res: Response, error: any) => any;
|
|
159
|
+
|
|
103
160
|
// List of paths to the necessary proto files for the gateway.
|
|
104
161
|
includeProtoRoots?: string[];
|
|
162
|
+
|
|
105
163
|
// Configuration of the path to the certificate in gRPC.
|
|
106
164
|
// Set to null to use system certificates by default.
|
|
107
165
|
caCertificatePath?: string | null;
|
|
166
|
+
|
|
108
167
|
// Telemetry sending configuration.
|
|
109
168
|
sendStats?: SendStats;
|
|
169
|
+
|
|
110
170
|
// Configuration of headers sent to the API.
|
|
111
171
|
proxyHeaders?: ProxyHeaders;
|
|
172
|
+
|
|
112
173
|
// When passing a boolean value, it enables/disables debug headers in the response to the request.
|
|
113
174
|
// For unary requests to gRPC backends, debug headers will include information from the trailing metadata returned by the backend.
|
|
114
175
|
withDebugHeaders?: boolean;
|
|
115
|
-
|
|
176
|
+
|
|
177
|
+
// Validation schema for parameters used when no schema is present in the action.
|
|
116
178
|
// You can use DEFAULT_VALIDATION_SCHEMA from lib/constants.ts.
|
|
117
179
|
validationSchema?: object;
|
|
180
|
+
|
|
118
181
|
// Enables encoding of REST path arguments (default is true).
|
|
119
182
|
encodePathArgs?: boolean;
|
|
183
|
+
|
|
120
184
|
// Configuration for automatic connection re-establishment upon connection error through L3 load balancer (default is true).
|
|
121
185
|
grpcRecreateService?: boolean;
|
|
122
|
-
|
|
186
|
+
|
|
187
|
+
// Customize retry behavior for grpc requests
|
|
188
|
+
grpcRetryCondition?: GrpcRetryCondition;
|
|
189
|
+
|
|
190
|
+
// Customize retry behavior for rest (axios) requests
|
|
191
|
+
axiosRetryCondition?: AxiosRetryCondition;
|
|
192
|
+
|
|
193
|
+
// Enable verification of response contentType header. Actual only for REST actions.
|
|
194
|
+
// This value can be set/redefined in the action config.
|
|
123
195
|
expectedResponseContentType?: ResponseContentType | ResponseContentType[];
|
|
196
|
+
|
|
197
|
+
// Function to get authentication arguments for API requests
|
|
198
|
+
getAuthArgs: (req: Request, res: Response) => Record<string, unknown> | undefined;
|
|
199
|
+
|
|
200
|
+
// Function to get authentication headers for API requests
|
|
201
|
+
getAuthHeaders: (params: GetAuthHeadersParams) => Record<string, string> | undefined;
|
|
202
|
+
|
|
203
|
+
// Error constructor for handling errors
|
|
204
|
+
ErrorConstructor: AppErrorConstructor;
|
|
124
205
|
}
|
|
125
206
|
```
|
|
126
207
|
|
|
127
|
-
|
|
208
|
+
### Validation Schema
|
|
209
|
+
|
|
210
|
+
By default, for path params in REST actions, the following regexp is used: `/^((?!(\.\.|\?|#|\\|\/)).)*$/i`.
|
|
211
|
+
If the parameter value does not pass validation, the `GATEWAY_INVALID_PARAM_VALUE` error is returned.
|
|
128
212
|
|
|
129
|
-
|
|
130
|
-
If the parameter value does not pass validation, the `GATEWAY_INVALID_PARAM_VALUE` value is returned.
|
|
213
|
+
You can use the `DEFAULT_VALIDATION_SCHEMA` from `lib/constants.ts` as a starting point:
|
|
131
214
|
|
|
132
|
-
|
|
215
|
+
```javascript
|
|
216
|
+
export const DEFAULT_VALIDATION_SCHEMA = {
|
|
217
|
+
additionalProperties: {
|
|
218
|
+
oneOf: [
|
|
219
|
+
{
|
|
220
|
+
type: 'number',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: 'string',
|
|
224
|
+
pattern: '^((?!(\\.\\.|\\?|#|\\\\|\\/)).)*$',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: 'object',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Using the API in Node.js
|
|
133
235
|
|
|
134
|
-
|
|
236
|
+
In addition to the Express controller, the gateway also exports an `api` object for making direct requests to backend services:
|
|
135
237
|
|
|
136
238
|
```javascript
|
|
137
239
|
import {getGatewayControllers} from '@gravity-ui/gateway';
|
|
@@ -146,15 +248,18 @@ const config = {
|
|
|
146
248
|
};
|
|
147
249
|
|
|
148
250
|
const {api: gatewayApi} = getGatewayControllers({root: Schema}, config);
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Subsequently, in the code, you can use it as follows:
|
|
152
251
|
|
|
153
|
-
|
|
154
|
-
gatewayApi
|
|
252
|
+
// Use the API to make requests
|
|
253
|
+
const result = await gatewayApi.serviceName.actionName({
|
|
254
|
+
authArgs: {token: 'auth-token'},
|
|
255
|
+
requestId: '123',
|
|
256
|
+
headers: {},
|
|
257
|
+
args: {param1: 'value1'},
|
|
258
|
+
ctx: context,
|
|
259
|
+
});
|
|
155
260
|
```
|
|
156
261
|
|
|
157
|
-
`actionConfig` has the following structure:
|
|
262
|
+
The `actionConfig` parameter has the following structure:
|
|
158
263
|
|
|
159
264
|
```typescript
|
|
160
265
|
interface ApiActionConfig<Context, TRequestData> {
|
|
@@ -171,50 +276,49 @@ interface ApiActionConfig<Context, TRequestData> {
|
|
|
171
276
|
### Schema Scopes
|
|
172
277
|
|
|
173
278
|
Each schema belongs to its own namespace. Service and action names between schemas are completely independent and can coincide. Each scope has an independent gRPC context, which eliminates naming conflicts between schemas in proto files.
|
|
174
|
-
|
|
279
|
+
|
|
280
|
+
The scope name is the key in the first parameter of the object containing the schemas:
|
|
175
281
|
|
|
176
282
|
```javascript
|
|
177
283
|
const schemasByScopes = {scope1: schema1, scope2: schema2};
|
|
178
284
|
```
|
|
179
285
|
|
|
180
|
-
Example with two scope namespaces: `root` and `anotherScope
|
|
286
|
+
Example with two scope namespaces: `root` and `anotherScope`:
|
|
181
287
|
|
|
182
288
|
```javascript
|
|
183
289
|
import {getGatewayControllers} from '@gravity-ui/gateway';
|
|
184
290
|
|
|
185
291
|
const {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} = getGatewayControllers({
|
|
292
|
+
controller, // Controller
|
|
293
|
+
api, // API (for Node.js environment)
|
|
294
|
+
} = getGatewayControllers({root: rootSchema, anotherScope: anotherSchema}, config);
|
|
189
295
|
|
|
190
|
-
// API calls are made by specifying the scope
|
|
191
|
-
const resultFromRoot = api.
|
|
192
|
-
const resultFromAnother = api.
|
|
296
|
+
// API calls are made by specifying the scope
|
|
297
|
+
const resultFromRoot = api.root.rootService.rootAction(params);
|
|
298
|
+
const resultFromAnother = api.anotherScope.anotherService.anotherAction(params);
|
|
193
299
|
```
|
|
194
300
|
|
|
195
|
-
There is a special scope called root
|
|
301
|
+
There is a special scope called `root`. Its methods can be invoked without explicitly specifying the scope:
|
|
196
302
|
|
|
197
303
|
```javascript
|
|
198
|
-
const resultFromRoot = api.
|
|
304
|
+
const resultFromRoot = api.root.rootService.rootAction(params);
|
|
199
305
|
// Same result
|
|
200
|
-
const sameResultFromRoot = api
|
|
306
|
+
const sameResultFromRoot = api.rootService.rootAction(params);
|
|
201
307
|
```
|
|
202
308
|
|
|
203
|
-
The controller for
|
|
309
|
+
The controller for expresskit will also expect the `:scope` parameter. If the scope parameter is not specified, the default scope is assumed to be `root`.
|
|
204
310
|
|
|
205
311
|
```javascript
|
|
206
312
|
{
|
|
207
|
-
'POST /<prefix>/:scope/:service/:action':
|
|
313
|
+
'POST /<prefix>/:scope/:service/:action': gatewayController
|
|
208
314
|
}
|
|
209
315
|
```
|
|
210
316
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
### Connecting a Specific Set of Actions
|
|
317
|
+
### Connecting Specific Actions
|
|
214
318
|
|
|
215
|
-
|
|
319
|
+
You can explicitly specify which actions to connect from the schemas using the `actions` field in the config. If actions are not provided, all actions from the schemas are connected by default.
|
|
216
320
|
|
|
217
|
-
```
|
|
321
|
+
```javascript
|
|
218
322
|
import {getGatewayControllers} from '@gravity-ui/gateway';
|
|
219
323
|
import rootSchema from '<schemas package>';
|
|
220
324
|
import localSchema from '../shared/schemas';
|
|
@@ -223,49 +327,221 @@ const config = {
|
|
|
223
327
|
installation: 'external',
|
|
224
328
|
env: 'production',
|
|
225
329
|
includeProtoRoots: ['...'],
|
|
226
|
-
actions: [
|
|
330
|
+
actions: [
|
|
331
|
+
'local.*', // All actions from the 'local' scope
|
|
332
|
+
'root.serviceA.*', // All actions from 'serviceA' in the 'root' scope
|
|
333
|
+
'root.serviceB.getUser', // Only the 'getUser' action from 'serviceB' in the 'root' scope
|
|
334
|
+
],
|
|
227
335
|
};
|
|
228
336
|
|
|
229
337
|
const {api: gatewayApi} = getGatewayControllers({root: rootSchema, local: localSchema}, config);
|
|
230
338
|
```
|
|
231
339
|
|
|
232
|
-
|
|
340
|
+
Available patterns for specifying actions:
|
|
341
|
+
|
|
342
|
+
- `<scope>.*` - all actions from the specified scope
|
|
343
|
+
- `<scope>.<service>.*` - all actions from the specified service
|
|
344
|
+
- `<scope>.<service>.<action>` - only the specified action
|
|
345
|
+
|
|
346
|
+
**Note:** This configuration only affects client-side access. All actions remain accessible on the server side.
|
|
347
|
+
|
|
348
|
+
### Overriding Endpoints
|
|
349
|
+
|
|
350
|
+
You can override specific endpoints using the `GATEWAY_ENDPOINTS_OVERRIDES` environment variable. This is useful for testing environments.
|
|
351
|
+
|
|
352
|
+
Example format:
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
GATEWAY_ENDPOINTS_OVERRIDES = JSON.stringify({
|
|
356
|
+
serviceName: {
|
|
357
|
+
endpoint: 'https://example.com',
|
|
358
|
+
},
|
|
359
|
+
'example.exampleService': {
|
|
360
|
+
endpoint: 'https://overrided.example.com',
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Authentication
|
|
366
|
+
|
|
367
|
+
The gateway supports set up authentication through the `getAuthArgs` and `getAuthHeaders` config options:
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
const config = {
|
|
371
|
+
// ...other config options
|
|
372
|
+
|
|
373
|
+
// Get authentication arguments for request
|
|
374
|
+
getAuthArgs: (req, res) => ({
|
|
375
|
+
token: req.authorization.token,
|
|
376
|
+
}),
|
|
377
|
+
|
|
378
|
+
// Generate authentication headers for backend requests
|
|
379
|
+
getAuthHeaders: (params) => {
|
|
380
|
+
if (!params?.token) return undefined;
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
Authorization: `Bearer ${params.token}`,
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
You can also define service-specific authentication by adding a `getAuthHeaders` function to individual actions:
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
const schema = {
|
|
393
|
+
userService: {
|
|
394
|
+
serviceName: 'users',
|
|
395
|
+
endpoints: {...},
|
|
396
|
+
actions: {
|
|
397
|
+
getProfile: {
|
|
398
|
+
path: () => '/profile',
|
|
399
|
+
method: 'GET',
|
|
400
|
+
getAuthHeaders: (params) => ({
|
|
401
|
+
'X-Special-Auth': params.token,
|
|
402
|
+
}),
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Error Handling
|
|
410
|
+
|
|
411
|
+
The gateway provides several ways to handle errors:
|
|
412
|
+
|
|
413
|
+
1. **Error constructor** through the `ErrorConstructor` (reqiured field) config option:
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
class CustomError extends Error {
|
|
417
|
+
constructor(message, options = {}) {
|
|
418
|
+
super(message);
|
|
419
|
+
this.name = 'CustomError';
|
|
420
|
+
this.code = options.code || 'UNKNOWN_ERROR';
|
|
421
|
+
this.status = options.status || 500;
|
|
422
|
+
this.details = options.details;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
static wrap(error) {
|
|
426
|
+
if (error instanceof CustomError) return error;
|
|
427
|
+
return new CustomError(error.message, {
|
|
428
|
+
code: error.code || 'INTERNAL_ERROR',
|
|
429
|
+
status: error.status || 500,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const config = {
|
|
435
|
+
// ...other config options
|
|
436
|
+
ErrorConstructor: CustomError,
|
|
437
|
+
};
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
2. **Custom request error handling** through the `onRequestFailed` config option:
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
const config = {
|
|
444
|
+
// ...other config options
|
|
445
|
+
onRequestFailed: (req, res, error) => {
|
|
446
|
+
console.error('Request failed:', error);
|
|
447
|
+
return res.status(error.status || 500).json({
|
|
448
|
+
error: error.message,
|
|
449
|
+
code: error.code,
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Retryable Errors
|
|
456
|
+
|
|
457
|
+
#### REST-actions
|
|
458
|
+
|
|
459
|
+
The **default** retry condition for REST-actions includes the following conditions:
|
|
460
|
+
|
|
461
|
+
- Network errors (detected by `axiosRetry.isNetworkError`)
|
|
462
|
+
- Other retryable errors (detected by `axiosRetry.isRetryableError`)
|
|
463
|
+
|
|
464
|
+
You can customize retry behavior for using the `axiosRetryCondition` config option:
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
const config = {
|
|
468
|
+
// ...other config options
|
|
469
|
+
axiosRetryCondition: (error) => {
|
|
470
|
+
// Custom logic to determine if the request should be retried
|
|
471
|
+
return error.code === 'TIMEOUT';
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### gRPC-actions
|
|
477
|
+
|
|
478
|
+
The **default** retry condition for gRPC-actions includes the certain gRPC status codes:
|
|
233
479
|
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
480
|
+
- `UNAVAILABLE`
|
|
481
|
+
- `CANCELLED`
|
|
482
|
+
- `ABORTED`
|
|
483
|
+
- `UNKNOWN`
|
|
237
484
|
|
|
238
|
-
|
|
485
|
+
You can customize retry behavior using the `grpcRetryCondition` config option:
|
|
239
486
|
|
|
240
|
-
|
|
487
|
+
```javascript
|
|
488
|
+
const config = {
|
|
489
|
+
// ...other config options
|
|
490
|
+
grpcRetryCondition: (error) => {
|
|
491
|
+
// Custom logic to determine if the request should be retried
|
|
492
|
+
return error.code === 'RESOURCE_EXHAUSTED';
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
```
|
|
241
496
|
|
|
242
|
-
|
|
497
|
+
For gRPC-requests that fail with `DEADLINE_EXCEEDED`, the service connection is recreated before retrying if config option `grpcRecreateService` is not set to `false`.
|
|
243
498
|
|
|
244
499
|
### gRPC Reflection for gRPC Actions
|
|
245
500
|
|
|
246
|
-
Instead of using gRPC proto files,
|
|
501
|
+
Instead of using gRPC proto files, you can use gRPC reflection to determine the structure of services and methods.
|
|
502
|
+
|
|
503
|
+
**Prerequisites:**
|
|
247
504
|
|
|
248
|
-
|
|
505
|
+
1. Install the `grpc-reflection-js` package:
|
|
249
506
|
|
|
250
|
-
|
|
507
|
+
```shell
|
|
508
|
+
npm install --save grpc-reflection-js
|
|
509
|
+
```
|
|
251
510
|
|
|
252
|
-
|
|
253
|
-
-
|
|
511
|
+
2. Apply patches to the `protobufjs` library:
|
|
512
|
+
- Add `npx gateway-reflection-patch` to your project's `postinstall` script. This assumes that protobufjs is located in the root of node_modules.
|
|
513
|
+
- Copy the patch from the library's patches folder to your project root, install [patch-package](https://www.npmjs.com/package/patch-package), and add the patch-package command to the `postinstall` script. In this case, you need to keep an eye on updates to the patches in the gateway when updating it.
|
|
254
514
|
|
|
255
|
-
|
|
515
|
+
If you encounter a "cannot run in wd [...]" error during Docker build, you can add unsafe-perm = true to your .npmrc file.
|
|
256
516
|
|
|
257
|
-
|
|
517
|
+
3. Configure your action to use reflection:
|
|
258
518
|
|
|
259
|
-
|
|
519
|
+
```javascript
|
|
520
|
+
import {GrpcReflection} from '@gravity-ui/gateway';
|
|
260
521
|
|
|
261
|
-
|
|
522
|
+
const schema = {
|
|
523
|
+
userService: {
|
|
524
|
+
serviceName: 'users',
|
|
525
|
+
endpoints: {...},
|
|
526
|
+
actions: {
|
|
527
|
+
getUser: {
|
|
528
|
+
protoKey: 'users.v1.UserService',
|
|
529
|
+
action: 'GetUser',
|
|
530
|
+
reflection: GrpcReflection.OnFirstRequest,
|
|
531
|
+
// Optional: refresh reflection cache every 3600 seconds (1 hour)
|
|
532
|
+
reflectionRefreshSec: 3600,
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
```
|
|
262
538
|
|
|
263
|
-
|
|
539
|
+
**Reflection Options:**
|
|
264
540
|
|
|
265
|
-
- `OnFirstRequest` - Perform reflection on the first
|
|
266
|
-
- `OnEveryRequest` - Perform reflection before every
|
|
541
|
+
- `GrpcReflection.OnFirstRequest` - Perform reflection on the first request. Use cached reflections.
|
|
542
|
+
- `GrpcReflection.OnEveryRequest` - Perform reflection before every request. Do not use cached reflections.
|
|
267
543
|
|
|
268
|
-
For the `OnFirstRequest` options you can specify the reflectionRefreshSec parameter, which indicates how often in seconds the reflection cache can be updated in the background. Cache updates happen asynchronously and don't block the current request. The initial reflection request with an empty cache might introduce some delay in the request.
|
|
544
|
+
For the `OnFirstRequest` options you can specify the `reflectionRefreshSec` parameter, which indicates how often in seconds the reflection cache can be updated in the background. Cache updates happen asynchronously and don't block the current request. The initial reflection request with an empty cache might introduce some delay in the request.
|
|
269
545
|
|
|
270
546
|
**Particularities**
|
|
271
547
|
|
|
@@ -282,3 +558,23 @@ For development, you need to apply the patch locally using the command `npx patc
|
|
|
282
558
|
**ChannelCredentials Type Mismatch Error**
|
|
283
559
|
|
|
284
560
|
This error can occur due to duplicate installations of the `@grpc/grpc-js` library. It's recommended to ensure that all versions of this library are aligned and consistent to avoid this issue.
|
|
561
|
+
|
|
562
|
+
## Development
|
|
563
|
+
|
|
564
|
+
### Running Tests
|
|
565
|
+
|
|
566
|
+
```shell
|
|
567
|
+
# Run unit tests
|
|
568
|
+
npm test
|
|
569
|
+
|
|
570
|
+
# Run integration tests
|
|
571
|
+
npm run test-integration
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Contributing
|
|
575
|
+
|
|
576
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
577
|
+
|
|
578
|
+
## License
|
|
579
|
+
|
|
580
|
+
MIT
|
package/build/components/grpc.js
CHANGED
|
@@ -600,13 +600,17 @@ function createGrpcAction({ root, credentials }, endpoints, config, serviceKey,
|
|
|
600
600
|
(0, grpc_1.isRecreateServiceError)(error);
|
|
601
601
|
const shouldRetry = error &&
|
|
602
602
|
retries &&
|
|
603
|
-
((_b = (_a = options.grpcRetryCondition) === null || _a === void 0 ? void 0 : _a.call(options, error)) !== null && _b !== void 0 ? _b : (0, grpc_1.
|
|
603
|
+
((_b = (_a = options.grpcRetryCondition) === null || _a === void 0 ? void 0 : _a.call(options, error)) !== null && _b !== void 0 ? _b : (0, grpc_1.isRetryableGrpcError)(error));
|
|
604
604
|
if (shouldRecreateService) {
|
|
605
605
|
ctx.log(`Service client for ${config.protoKey} is going to be re-created`);
|
|
606
606
|
recreateService(service, timeout * 1.5, ctx, args);
|
|
607
607
|
}
|
|
608
608
|
if (shouldRetry) {
|
|
609
|
-
|
|
609
|
+
(0, common_2.handleError)(ErrorConstructor, error, ctx, `Request failed, retrying ${retries--} more times`, {
|
|
610
|
+
serviceName,
|
|
611
|
+
actionName,
|
|
612
|
+
debugHeaders: (0, common_2.sanitizeDebugHeaders)(debugHeaders),
|
|
613
|
+
});
|
|
610
614
|
// Update pointer to re-created client in local service variable
|
|
611
615
|
try {
|
|
612
616
|
service = await getService(args);
|
package/build/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ApiWithRoot, GatewayConfig, GatewayRequest, GatewayResponse, SchemasByS
|
|
|
2
2
|
import { GatewayContext } from './models/context';
|
|
3
3
|
export * from './utils/typed-api';
|
|
4
4
|
export * from './utils/grpc-reflection';
|
|
5
|
-
export {
|
|
5
|
+
export { isRetryableGrpcError } from './utils/grpc';
|
|
6
6
|
export * from './models/common';
|
|
7
7
|
export * from './models/context';
|
|
8
8
|
export * from './models/error';
|
package/build/index.js
CHANGED
|
@@ -40,7 +40,7 @@ const overrideEndpoints_1 = __importDefault(require("./utils/overrideEndpoints")
|
|
|
40
40
|
__exportStar(require("./utils/typed-api"), exports);
|
|
41
41
|
__exportStar(require("./utils/grpc-reflection"), exports);
|
|
42
42
|
var grpc_2 = require("./utils/grpc");
|
|
43
|
-
Object.defineProperty(exports, "isRetryableGrpcError", { enumerable: true, get: function () { return grpc_2.
|
|
43
|
+
Object.defineProperty(exports, "isRetryableGrpcError", { enumerable: true, get: function () { return grpc_2.isRetryableGrpcError; } });
|
|
44
44
|
__exportStar(require("./models/common"), exports);
|
|
45
45
|
__exportStar(require("./models/context"), exports);
|
|
46
46
|
__exportStar(require("./models/error"), exports);
|
package/build/utils/grpc.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as grpc from '@grpc/grpc-js';
|
|
2
2
|
import * as protobufjs from 'protobufjs';
|
|
3
3
|
export declare function decodeAnyMessageRecursively(root: protobufjs.Root, message?: unknown, decodeAnyMessageProtoLoaderOptions?: protobufjs.IConversionOptions): unknown;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function isRetryableGrpcError(error?: grpc.ServiceError): boolean;
|
|
5
5
|
export declare function isRecreateServiceError(error?: grpc.ServiceError): boolean;
|
package/build/utils/grpc.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* eslint-disable camelcase */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.isRecreateServiceError = exports.
|
|
4
|
+
exports.isRecreateServiceError = exports.isRetryableGrpcError = exports.decodeAnyMessageRecursively = void 0;
|
|
5
5
|
const constants_1 = require("../constants");
|
|
6
6
|
function isEncodedMessage(message) {
|
|
7
7
|
return Boolean(message.type_url && message.value);
|
|
@@ -40,13 +40,13 @@ function decodeAnyMessageRecursively(root, message, decodeAnyMessageProtoLoaderO
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
exports.decodeAnyMessageRecursively = decodeAnyMessageRecursively;
|
|
43
|
-
function
|
|
43
|
+
function isRetryableGrpcError(error) {
|
|
44
44
|
if (!error) {
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
return constants_1.RETRYABLE_STATUS_CODES.includes(error.code);
|
|
48
48
|
}
|
|
49
|
-
exports.
|
|
49
|
+
exports.isRetryableGrpcError = isRetryableGrpcError;
|
|
50
50
|
function isRecreateServiceError(error) {
|
|
51
51
|
if (!error) {
|
|
52
52
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravity-ui/gateway",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@grpc/grpc-js": "^1.9.9",
|
|
43
43
|
"@grpc/proto-loader": "^0.7.8",
|
|
44
44
|
"ajv": "^8.12.0",
|
|
45
|
-
"axios": "^1.3
|
|
46
|
-
"axios-retry": "^3.
|
|
45
|
+
"axios": "^1.8.3",
|
|
46
|
+
"axios-retry": "^3.9.1",
|
|
47
47
|
"lodash": "^4.17.21",
|
|
48
48
|
"object-sizeof": "^2.6.5",
|
|
49
49
|
"protobufjs": "^7.2.5",
|