{"id":24755171,"url":"https://github.com/gravity-ui/gateway","last_synced_at":"2026-01-19T13:00:53.182Z","repository":{"id":184455105,"uuid":"667358019","full_name":"gravity-ui/gateway","owner":"gravity-ui","description":"Express controller for working with REST/GRPC APIs","archived":false,"fork":false,"pushed_at":"2025-12-10T20:34:10.000Z","size":2272,"stargazers_count":8,"open_issues_count":1,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-12-11T07:43:45.517Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gravity-ui.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-07-17T10:20:21.000Z","updated_at":"2025-12-10T20:33:18.000Z","dependencies_parsed_at":"2023-11-06T11:28:15.975Z","dependency_job_id":"03475c7a-805f-4a6e-9b31-ced961d2ea20","html_url":"https://github.com/gravity-ui/gateway","commit_stats":null,"previous_names":["gravity-ui/gateway"],"tags_count":71,"template":false,"template_full_name":"gravity-ui/package-example","purl":"pkg:github/gravity-ui/gateway","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fgateway","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fgateway/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fgateway/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fgateway/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gravity-ui","download_url":"https://codeload.github.com/gravity-ui/gateway/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fgateway/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28568833,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-19T12:50:50.164Z","status":"ssl_error","status_checked_at":"2026-01-19T12:50:42.704Z","response_time":67,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-28T12:36:46.644Z","updated_at":"2026-01-19T13:00:53.140Z","avatar_url":"https://github.com/gravity-ui.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @gravity-ui/gateway \u0026middot; [![npm package](https://img.shields.io/npm/v/@gravity-ui/gateway)](https://www.npmjs.com/package/@gravity-ui/gateway) [![CI](https://img.shields.io/github/actions/workflow/status/gravity-ui/gateway/.github/workflows/ci.yml?label=CI\u0026logo=github)](https://github.com/gravity-ui/gateway/actions/workflows/ci.yml?query=branch:main)\n\nA flexible and powerful Express controller for working with REST and gRPC APIs in Node.js applications.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Basic Usage](#basic-usage)\n- [Configuration](#configuration)\n  - [`proxyHeaders`](#proxyheaders)\n  - [Validation Schema](#validation-schema)\n  - [Using the API in Node.js](#using-the-api-in-nodejs)\n  - [Schema Scopes](#schema-scopes)\n  - [Connecting Specific Actions](#connecting-specific-actions)\n  - [Overriding Endpoints](#overriding-endpoints)\n  - [Authentication](#authentication)\n  - [Error Handling](#error-handling)\n  - [Retryable Errors](#retryable-errors)\n    - [REST-actions](#rest-actions)\n    - [gRPC-actions](#grpc-actions)\n  - [Request Cancellation](#request-cancellation)\n  - [Response Content Type Validation](#response-content-type-validation)\n  - [gRPC Reflection for gRPC Actions](#grpc-reflection-for-grpc-actions)\n- [Development](#development)\n  - [Running Tests](#running-tests)\n  - [Contributing](#contributing)\n- [License](#license)\n\n## Installation\n\n```shell\nnpm install --save @gravity-ui/gateway\n```\n\n## Basic Usage\n\nFirst, create a controller by importing Gateway and your API schemas:\n\n```javascript\nimport {getGatewayControllers} from '@gravity-ui/gateway';\nimport Schema from '\u003cschemas package\u003e';\n\nconst config = {\n  installation: 'external',\n  env: 'production',\n  includeProtoRoots: ['...'],\n};\n\nconst {controller: gatewayController} = getGatewayControllers({root: Schema}, config);\n\nexport default gatewayController;\n```\n\nThen, connect the controller to your Express routes (using [expresskit](https://github.com/gravity-ui/expresskit)):\n\n```javascript\n{\n    'POST   /\u003cprefix\u003e/:scope/:service/:action': {target: '\u003ccontroller\u003e', afterAuth: ['credentials']}\n}\n```\n\nThe `prefix` can be any prefix for API endpoints (for example, `/gateway/:service/:action`).\n\n## Configuration\n\n```typescript\nimport {AxiosRequestConfig} from 'axios';\nimport {IncomingHttpHeaders} from 'http';\nimport {IAxiosRetryConfig} from 'axios-retry';\n\ninterface OnUnknownActionData {\n  service?: string;\n  action?: string;\n}\n\ninterface Stats {\n  service: string;\n  action: string;\n  restStatus: number;\n  grpcStatus?: number;\n  responseSize: number;\n  requestId: string;\n  requestTime: number;\n  requestMethod: string;\n  requestUrl: string;\n  timestamp: number;\n  userId?: string;\n  traceId: string;\n}\n\ntype SendStats = (\n  stats: Stats,\n  headers: IncomingHttpHeaders,\n  ctx: CoreContext,\n  meta: {debugHeaders: Headers},\n) =\u003e void;\n\ntype GrpcRetryCondition = (error: ServiceError) =\u003e boolean;\ntype AxiosRetryCondition = IAxiosRetryConfig['retryCondition'];\n\ntype ControllerType = 'rest' | 'grpc';\n\ntype ProxyHeadersFunctionExtra = {\n  service: string;\n  action: string;\n\n  protopath?: string;\n  protokey?: string;\n};\n\ntype ProxyHeadersFunction = (\n  headers: IncomingHttpHeaders,\n  type: ControllerType,\n  extra: ProxyHeadersFunctionExtra,\n) =\u003e IncomingHttpHeaders;\ntype ProxyHeaders = string[] | ProxyHeadersFunction;\ntype ResponseContentType = AxiosResponse['headers']['Content-Type'];\n\ntype GetAuthHeadersParams\u003cAuthArgs = Record\u003cstring, unknown\u003e\u003e = {\n  actionType: ControllerType;\n  serviceName: string;\n  requestHeaders: Headers;\n  authArgs: AuthArgs | undefined;\n};\n\ninterface AppErrorArgs {\n  code?: string | number;\n  details?: object;\n  debug?: object;\n}\n\ninterface AppErrorWrapArgs extends AppErrorArgs {\n  message?: string;\n}\n\ninterface AppErrorConstructor {\n  new (message?: string, args?: AppErrorArgs): Error;\n\n  wrap: (error: Error, args?: AppErrorWrapArgs) =\u003e Error;\n}\n\ninterface GatewayConfig {\n  // Gateway Installation (external/internal/...). If not provided, determined from process.env.APP_INSTALLATION.\n  installation?: string;\n\n  // Gateway Environment (production/testing/...). If not provided, determined from process.env.APP_ENV.\n  env?: string;\n\n  // Additional gRPC client options.\n  grpcOptions?: object;\n\n  // Additional Axios client options.\n  axiosConfig?: AxiosRequestConfig;\n\n  // List of actions to connect from the schema. By default, all actions are connected.\n  actions?: string[];\n\n  // Called when an unknown service or action is provided.\n  onUnknownAction?: (req: Request, res: Response, data: OnUnknownActionData) =\u003e any;\n\n  // Called before the request is executed.\n  onBeforeAction?: (\n    req: Request,\n    res: Response,\n    scope: string,\n    service: string,\n    action: string,\n    config?: ApiServiceActionConfig,\n  ) =\u003e any;\n\n  // Called upon successful completion of the request.\n  onRequestSuccess?: (req: Request, res: Response, data: any) =\u003e any;\n\n  // Called in case of unsuccessful request execution.\n  onRequestFailed?: (req: Request, res: Response, error: any) =\u003e any;\n\n  // List of paths to the necessary proto files for the gateway.\n  includeProtoRoots?: string[];\n\n  // Configuration of the path to the CA certificate in gRPC.\n  // Set to null to use system certificates by default.\n  caCertificatePath?: string | null;\n  // Configuration of the path to the client certificate for mTLS in gRPC.\n  clientCertificatePath?: string | null;\n  // Configuration of the path to the client private key for mTLS in gRPC.\n  clientKeyPath?: string | null;\n  // Telemetry sending configuration.\n  sendStats?: SendStats;\n\n  // Configuration of headers sent to the API.\n  proxyHeaders?: ProxyHeaders;\n\n  // When passing a boolean value, it enables/disables debug headers in the response to the request.\n  // For unary requests to gRPC backends, debug headers will include information from the trailing metadata returned by the backend.\n  withDebugHeaders?: boolean | ((req: Request, res: Response) =\u003e boolean);\n\n  // Validation schema for parameters used when no schema is present in the action.\n  // You can use DEFAULT_VALIDATION_SCHEMA from lib/constants.ts.\n  validationSchema?: object;\n\n  // Enables encoding of REST path arguments (default is true).\n  encodePathArgs?: boolean;\n\n  // Configuration for automatic connection re-establishment upon connection error through L3 load balancer (default is true).\n  grpcRecreateService?: boolean;\n\n  // Customize retry behavior for grpc requests\n  grpcRetryCondition?: GrpcRetryCondition;\n\n  // Customize retry behavior for rest (axios) requests\n  axiosRetryCondition?: AxiosRetryCondition;\n\n  // Enable verification of response contentType header. Actual only for REST actions.\n  // This value can be set/redefined in the action config.\n  expectedResponseContentType?: ResponseContentType | ResponseContentType[];\n\n  // Function to get authentication arguments for API requests\n  getAuthArgs: (req: Request, res: Response) =\u003e Record\u003cstring, unknown\u003e | undefined;\n\n  // Function to get authentication headers for API requests\n  getAuthHeaders: (params: GetAuthHeadersParams) =\u003e Record\u003cstring, string\u003e | undefined;\n\n  // Error constructor for handling errors\n  ErrorConstructor: AppErrorConstructor;\n\n  // Axios interceptors configuration\n  axiosInterceptors?: AxiosInterceptorsConfig;\n}\n```\n\n### `proxyHeaders`\n\n`GatewayConfig.proxyHeaders` is an optional method that allows setting headers for requests at the entire `gateway` level:\n\n```javascript\nconst proxyHeaders = (headers, actionType, extra) =\u003e {\n  const normalizedHeaders = {...headers};\n  const {service, action, protopath, protokey} = extra;\n\n  if (actionType === 'rest' \u0026\u0026 service === 'mail') {\n    normalizedHeaders['x-mail-service-action'] = action;\n  }\n\n  return normalizedHeaders;\n};\n\nconst {controller: gatewayController} = getGatewayControllers(\n  {root: Schema},\n  {...config, proxyHeaders},\n);\n```\n\nThe `extra` parameter contains additional information about the request:\n\n- `service`: The service name\n- `action`: The action name\n- `protopath`: The proto path (for gRPC actions)\n- `protokey`: The proto key (for gRPC actions)\n\nWhen accessing headers, the gateway first tries to get the header with the exact case provided by the user. If the header doesn't exist with that exact case, it falls back to looking for the same header name in lowercase. This behavior ensures compatibility with various HTTP clients that might send headers with different casing.\n\nYou can set headers for a specific action using `ApiServiceBaseActionConfig.proxyHeaders`:\n\n```javascript\nconst schema = {\n  userService: {\n    serviceName: 'users',\n    endpoints: {...},\n    actions: {\n      getProfile: {\n        path: () =\u003e '/profile',\n        method: 'GET',\n        proxyHeaders: (headers) =\u003e ({...headers, ['x-users-service-action']: 'get-profile'}),\n      },\n    },\n  },\n};\n```\n\nThe `GatewayConfig.proxyHeaders` and `ApiServiceBaseActionConfig.proxyHeaders` are merged when the action is called. The strategy for merging headers is not guaranteed.\n\nIt is recommended to use `GatewayConfig.proxyHeaders` for assigning headers that are common to the entire application or a large number of actions. Otherwise, it is preferable to use `ApiServiceBaseActionConfig.proxyHeaders`.\n\n### Validation Schema\n\nBy default, for path params in REST actions, the following regexp is used: `/^((?!(\\.\\.|\\?|#|\\\\|\\/)).)*$/i`.\nIf the parameter value does not pass validation, the `GATEWAY_INVALID_PARAM_VALUE` error is returned.\n\nYou can use the `DEFAULT_VALIDATION_SCHEMA` from `lib/constants.ts` as a starting point:\n\n```javascript\nexport const DEFAULT_VALIDATION_SCHEMA = {\n  additionalProperties: {\n    oneOf: [\n      {\n        type: 'number',\n      },\n      {\n        type: 'string',\n        pattern: '^((?!(\\\\.\\\\.|\\\\?|#|\\\\\\\\|\\\\/)).)*$',\n      },\n      {\n        type: 'object',\n      },\n    ],\n  },\n};\n```\n\n### Using the API in Node.js\n\nIn addition to the Express controller, the gateway also exports an `api` object for making direct requests to backend services:\n\n```javascript\nimport {getGatewayControllers} from '@gravity-ui/gateway';\nimport Schema from '\u003cschemas package\u003e';\n\nconst config = {\n  installation: 'external',\n  env: 'production',\n  includeProtoRoots: ['...'],\n  timeout: 25000, // default 25 seconds\n  caCertificatePath: '...',\n  // Optional: paths for mTLS client certificate and key\n  clientCertificatePath: '...',\n  clientKeyPath: '...',\n};\n\nconst {api: gatewayApi} = getGatewayControllers({root: Schema}, config);\n\n// Use the API to make requests\nconst result = await gatewayApi.serviceName.actionName({\n  authArgs: {token: 'auth-token'},\n  requestId: '123',\n  headers: {},\n  args: {param1: 'value1'},\n  ctx: context,\n});\n```\n\nThe `actionConfig` parameter has the following structure:\n\n```typescript\ninterface ApiActionConfig\u003cContext, TRequestData\u003e {\n  requestId: string;\n  headers: Headers;\n  args: TRequestData;\n  ctx: Context;\n  timeout?: number;\n  callback?: (response: TResponseData) =\u003e void;\n  authArgs?: Record\u003cstring, unknown\u003e;\n  userId?: string;\n  abortSignal?: AbortSignal;\n}\n```\n\n### Schema Scopes\n\nEach 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.\n\nThe scope name is the key in the first parameter of the object containing the schemas:\n\n```javascript\nconst schemasByScopes = {scope1: schema1, scope2: schema2};\n```\n\nExample with two scope namespaces: `root` and `anotherScope`:\n\n```javascript\nimport {getGatewayControllers} from '@gravity-ui/gateway';\n\nconst {\n  controller, // Controller\n  api, // API (for Node.js environment)\n} = getGatewayControllers({root: rootSchema, anotherScope: anotherSchema}, config);\n\n// API calls are made by specifying the scope\nconst resultFromRoot = api.root.rootService.rootAction(params);\nconst resultFromAnother = api.anotherScope.anotherService.anotherAction(params);\n```\n\nThere is a special scope called `root`. Its methods can be invoked without explicitly specifying the scope:\n\n```javascript\nconst resultFromRoot = api.root.rootService.rootAction(params);\n// Same result\nconst sameResultFromRoot = api.rootService.rootAction(params);\n```\n\nThe controller for expresskit will also expect the `:scope` parameter. If the scope parameter is not specified, the default scope is assumed to be `root`.\n\n```javascript\n{\n    'POST   /\u003cprefix\u003e/:scope/:service/:action': gatewayController\n}\n```\n\n### Connecting Specific Actions\n\nYou 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.\n\n```javascript\nimport {getGatewayControllers} from '@gravity-ui/gateway';\nimport rootSchema from '\u003cschemas package\u003e';\nimport localSchema from '../shared/schemas';\n\nconst config = {\n  installation: 'external',\n  env: 'production',\n  includeProtoRoots: ['...'],\n  actions: [\n    'local.*', // All actions from the 'local' scope\n    'root.serviceA.*', // All actions from 'serviceA' in the 'root' scope\n    'root.serviceB.getUser', // Only the 'getUser' action from 'serviceB' in the 'root' scope\n  ],\n};\n\nconst {api: gatewayApi} = getGatewayControllers({root: rootSchema, local: localSchema}, config);\n```\n\nAvailable patterns for specifying actions:\n\n- `\u003cscope\u003e.*` - all actions from the specified scope\n- `\u003cscope\u003e.\u003cservice\u003e.*` - all actions from the specified service\n- `\u003cscope\u003e.\u003cservice\u003e.\u003caction\u003e` - only the specified action\n\n**Note:** This configuration only affects client-side access. All actions remain accessible on the server side.\n\n### Overriding Endpoints\n\nYou can override specific endpoints using the `GATEWAY_ENDPOINTS_OVERRIDES` environment variable. This is useful for testing environments.\n\nExample format:\n\n```javascript\nGATEWAY_ENDPOINTS_OVERRIDES = JSON.stringify({\n  serviceName: {\n    endpoint: 'https://example.com',\n  },\n  'example.exampleService': {\n    endpoint: 'https://overrided.example.com',\n  },\n});\n```\n\n### Authentication\n\nThe gateway supports set up authentication through the `getAuthArgs` and `getAuthHeaders` config options:\n\n```javascript\nconst config = {\n  // ...other config options\n\n  // Get authentication arguments for request\n  getAuthArgs: (req, res) =\u003e ({\n    token: req.authorization.token,\n  }),\n\n  // Generate authentication headers for backend requests\n  getAuthHeaders: (params) =\u003e {\n    if (!params?.token) return undefined;\n\n    return {\n      Authorization: `Bearer ${params.token}`,\n    };\n  },\n};\n```\n\nYou can define authentication at three levels:\n|\n\n1. **Gateway level** (global) - as shown above\n2. **Service level** - by adding authentication methods to the service definition\n3. **Action level** (most specific) - by adding authentication methods to individual actions\n   |\n   The authentication methods are checked in the following order: action \u003e service \u003e gateway.\n   |\n   **Service-level authentication:**\n   |\n\n```javascript\nconst schema = {\n  userService: {\n    serviceName: 'users',\n    endpoints: {...},\n    // Service-level authentication\n    getAuthHeaders: (params) =\u003e ({\n      'X-User-Service-Auth': params.token,\n    }),\n    getAuthArgs: (req, res) =\u003e ({\n      token: req.authorization.token,\n      serviceSpecificData: req.headers['x-service-data'],\n    }),\n    actions: {\n      getProfile: {\n        path: () =\u003e '/profile',\n        method: 'GET',\n        // Uses service-level authentication\n      },\n      updateProfile: {\n        path: () =\u003e '/profile',\n        method: 'PUT',\n        // Action-level authentication (overrides service-level)\n        getAuthHeaders: (params) =\u003e ({\n          'X-Special-Auth': params.token,\n        }),\n      },\n    },\n  },\n};\n```\n\n### Error Handling\n\nThe gateway provides several ways to handle errors:\n\n1. **Error constructor** through the `ErrorConstructor` (reqiured field) config option:\n\n```javascript\nclass CustomError extends Error {\n  constructor(message, options = {}) {\n    super(message);\n    this.name = 'CustomError';\n    this.code = options.code || 'UNKNOWN_ERROR';\n    this.status = options.status || 500;\n    this.details = options.details;\n  }\n\n  static wrap(error) {\n    if (error instanceof CustomError) return error;\n    return new CustomError(error.message, {\n      code: error.code || 'INTERNAL_ERROR',\n      status: error.status || 500,\n    });\n  }\n}\n\nconst config = {\n  // ...other config options\n  ErrorConstructor: CustomError,\n};\n```\n\n2. **Custom request error handling** through the `onRequestFailed` config option:\n\n```javascript\nconst config = {\n  // ...other config options\n  onRequestFailed: (req, res, error) =\u003e {\n    console.error('Request failed:', error);\n    return res.status(error.status || 500).json({\n      error: error.message,\n      code: error.code,\n    });\n  },\n};\n```\n\n### Retryable Errors\n\n#### REST-actions\n\nThe **default** retry condition for REST-actions includes the following conditions:\n\n- Network errors (detected by `axiosRetry.isNetworkError`)\n- Other retryable errors (detected by `axiosRetry.isRetryableError`)\n\nYou can customize retry behavior using the `axiosRetryCondition` config option:\n\n```javascript\nconst config = {\n  // ...other config options\n  axiosRetryCondition: (error) =\u003e {\n    // Custom logic to determine if the request should be retried\n    return error.code === 'TIMEOUT';\n  },\n};\n```\n\nYou can also set retry conditions at the action level:\n\n```javascript\nconst schema = {\n  userService: {\n    serviceName: 'users',\n    endpoints: {...},\n    actions: {\n      getProfile: {\n        path: () =\u003e '/profile',\n        method: 'GET',\n        axiosRetryCondition: (error) =\u003e {\n          // Custom logic for this specific action\n          return error.code === 'ECONNRESET';\n        },\n      },\n    },\n  },\n};\n```\n\n#### gRPC-actions\n\nThe **default** retry condition for gRPC-actions includes the certain gRPC status codes:\n\n- `UNAVAILABLE`\n- `CANCELLED`\n- `ABORTED`\n- `UNKNOWN`\n\nYou can customize retry behavior using the `grpcRetryCondition` config option:\n\n```javascript\nconst config = {\n  // ...other config options\n  grpcRetryCondition: (error) =\u003e {\n    // Custom logic to determine if the request should be retried\n    return error.code === 'RESOURCE_EXHAUSTED';\n  },\n};\n```\n\nThe library exports the `isRetryableGrpcError` function that you can use to check if a gRPC error is retryable according to the default conditions:\n\n```javascript\nimport {isRetryableGrpcError} from '@gravity-ui/gateway';\n\n// Use in your custom retry condition\nconst customGrpcRetryCondition = (error) =\u003e {\n  return isRetryableGrpcError(error) || error.code === 'RESOURCE_EXHAUSTED';\n};\n```\n\nFor gRPC-requests that fail with `DEADLINE_EXCEEDED`, the service connection is recreated before retrying if config option `grpcRecreateService` is not set to `false`.\n\n### Request Cancellation\n\nThe gateway supports cancelling requests when the client disconnects. This is useful for long-running operations where you want to avoid unnecessary processing if the client is no longer waiting for the response.\n\nThis feature is enabled by default for exported controller. For API requests, you can pass an `AbortSignal` to cancel the request:\n\n```javascript\nconst abortController = new AbortController();\n\nconst result = await gatewayApi.serviceName.actionName({\n  authArgs: {token: 'auth-token'},\n  requestId: '123',\n  headers: {},\n  args: {param1: 'value1'},\n  ctx: context,\n  abortSignal: abortController.signal,\n});\n```\n\nYou can also control this behavior at the action level using the `abortOnClientDisconnect` option:\n\n```javascript\nconst schema = {\n  userService: {\n    serviceName: 'users',\n    endpoints: {...},\n    actions: {\n      longRunningOperation: {\n        path: () =\u003e '/process',\n        method: 'POST',\n        abortOnClientDisconnect: true, // Enable cancellation for this action\n      },\n    },\n  },\n};\n```\n\n### Response Content Type Validation\n\nFor REST actions, you can validate the content type of the response to ensure it matches your expectations. This is useful for ensuring that the API returns the expected format.\n\nYou can set the expected content type at the gateway level:\n\n```javascript\nconst config = {\n  // ...other config options\n  expectedResponseContentType: 'application/json',\n};\n```\n\nOr at the action level:\n\n```javascript\nconst schema = {\n  userService: {\n    serviceName: 'users',\n    endpoints: {...},\n    actions: {\n      getProfile: {\n        path: () =\u003e '/profile',\n        method: 'GET',\n        expectedResponseContentType: 'application/json',\n      },\n      getDocument: {\n        path: () =\u003e '/document',\n        method: 'GET',\n        expectedResponseContentType: ['application/pdf', 'application/octet-stream'],\n      },\n    },\n  },\n};\n```\n\nYou can specify either a single content type or an array of acceptable content types. If the response content type doesn't match any of the expected types, an error will be thrown.\n\n### gRPC Reflection for gRPC Actions\n\nInstead of using gRPC proto files, you can use gRPC reflection to determine the structure of services and methods.\n\n**Prerequisites:**\n\n1. Install the `grpc-reflection-js` package:\n\n   ```shell\n   npm install --save grpc-reflection-js\n   ```\n\n2. Apply patches to the `protobufjs` library:\n   - Add `npx gateway-reflection-patch` to your project's `postinstall` script. This assumes that protobufjs is located in the root of node_modules.\n   - 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.\n\nIf you encounter a \"cannot run in wd [...]\" error during Docker build, you can add unsafe-perm = true to your .npmrc file.\n\n3. Configure your action to use reflection:\n\n   ```javascript\n   import {GrpcReflection} from '@gravity-ui/gateway';\n\n   const schema = {\n     userService: {\n       serviceName: 'users',\n       endpoints: {...},\n       actions: {\n         getUser: {\n           protoKey: 'users.v1.UserService',\n           action: 'GetUser',\n           reflection: GrpcReflection.OnFirstRequest,\n           // Optional: refresh reflection cache every 3600 seconds (1 hour)\n           reflectionRefreshSec: 3600,\n         },\n       },\n     },\n   };\n   ```\n\n**Reflection Options:**\n\n- `GrpcReflection.OnFirstRequest` - Perform reflection on the first request. Use cached reflections.\n- `GrpcReflection.OnEveryRequest` - Perform reflection before every request. Do not use cached reflections.\n\nFor 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.\n\n**Particularities**\n\nThe cache key for reflections consists of `protoKey` and `endpoint`. Therefore, actions with shared keys will use a common cached version, which will be obtained from the earliest scenario (when the first action request with the `OnFirstRequest` strategy is made).\n\nThis function is experimental. Fixes have been applied to `protobufjs` using [patch-package](https://github.com/ds300/patch-package) based on the following PRs:\n\n- Conversion of parameter names to camelCase [PR 1073](https://github.com/protobufjs/protobuf.js/pull/1073)\n- Fix for handling Map [PR 1478](https://github.com/protobufjs/protobuf.js/pull/1478)\n  grpc-reflection-js has also been patched to support custom options.\n\nFor development, you need to apply the patch locally using the command `npx patch-package`.\n\n**ChannelCredentials Type Mismatch Error**\n\nThis 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.\n\n## Development\n\n### Running Tests\n\n```shell\n# Run unit tests\nnpm test\n\n# Run integration tests\nnpm run test-integration\n```\n\n### Contributing\n\nContributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgravity-ui%2Fgateway","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgravity-ui%2Fgateway","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgravity-ui%2Fgateway/lists"}