RPC вызовы

RPC (Remote Procedure Call) в веб-разработке обычно реализуется поверх HTTP и JSON, где клиент отправляет запрос с описанием вызываемой процедуры и параметров, а сервер возвращает результат выполнения или ошибку. На практике распространён формат JSON-RPC 2.0, в котором сообщение имеет строго определённую структуру:

  • jsonrpc — версия протокола
  • method — имя вызываемой процедуры
  • params — параметры вызова
  • id — идентификатор запроса для сопоставления ответа

Пример запроса:

{
  "jsonrpc": "2.0",
  "method": "user.create",
  "params": {
    "name": "Ivan",
    "age": 30
  },
  "id": 1
}

Ответ:

{
  "jsonrpc": "2.0",
  "result": {
    "userId": 42
  },
  "id": 1
}

Ошибка:

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32602,
    "message": "Invalid params"
  },
  "id": 1
}

Строгость структуры делает RPC удобным кандидатом для валидации через JSON Schema и библиотеку Ajv.


Роль Ajv в валидации RPC-сообщений

Ajv (Another JSON Schema Validator) обеспечивает проверку JSON-данных на соответствие JSON Schema. В контексте RPC это позволяет:

  • контролировать корректность входящих запросов
  • предотвращать вызов методов с неверными параметрами
  • стандартизировать формат ошибок
  • валидировать ответы перед отправкой клиенту

Основная идея заключается в том, что каждая часть RPC-протокола описывается схемой, а Ajv выполняет компиляцию и проверку данных.


Базовая схема JSON-RPC запроса

Схема запроса может быть описана следующим образом:

const requestSchema = {
  type: "object",
  additionalProperties: false,
  required: ["jsonrpc", "method", "params", "id"],
  properties: {
    jsonrpc: {
      type: "string",
      const: "2.0"
    },
    method: {
      type: "string",
      minLength: 1
    },
    params: {
      type: "object"
    },
    id: {
      type: ["string", "number", "null"]
    }
  }
};

Компиляция схемы в Ajv:

import Ajv from "ajv";

const ajv = new Ajv();
const validateRequest = ajv.compile(requestSchema);

Проверка входящего сообщения:

const isValid = validateRequest(data);

if (!isValid) {
  console.log(validateRequest.errors);
}

Валидация параметров методов RPC

RPC-методы имеют разные сигнатуры, поэтому универсальная схема params часто заменяется на специфические схемы для каждого метода.

Пример схемы для метода user.create:

const createUserParamsSchema = {
  type: "object",
  additionalProperties: false,
  required: ["name", "age"],
  properties: {
    name: {
      type: "string",
      minLength: 1
    },
    age: {
      type: "integer",
      minimum: 0
    }
  }
};

Далее создаётся реестр схем по методам:

const methodSchemas = {
  "user.create": ajv.compile(createUserParamsSchema)
};

Использование при обработке RPC:

function validateMethod(method, params) {
  const validate = methodSchemas[method];

  if (!validate) {
    return { valid: false, error: "Unknown method" };
  }

  const valid = validate(params);

  return {
    valid,
    errors: validate.errors
  };
}

Валидация ответа RPC

Хотя чаще валидируются входящие данные, контроль исходящих ответов повышает стабильность API.

Схема успешного ответа:

const successResponseSchema = {
  type: "object",
  required: ["jsonrpc", "result", "id"],
  additionalProperties: false,
  properties: {
    jsonrpc: { const: "2.0" },
    result: {},
    id: {
      type: ["string", "number", "null"]
    }
  }
};

Схема ошибки:

const errorResponseSchema = {
  type: "object",
  required: ["jsonrpc", "error", "id"],
  additionalProperties: false,
  properties: {
    jsonrpc: { const: "2.0" },
    error: {
      type: "object",
      required: ["code", "message"],
      properties: {
        code: { type: "integer" },
        message: { type: "string" },
        data: {}
      },
      additionalProperties: true
    },
    id: {
      type: ["string", "number", "null"]
    }
  }
};

Обработка batch RPC-запросов

JSON-RPC поддерживает пакетные вызовы — массив запросов в одном HTTP-запросе.

Схема batch:

const batchSchema = {
  type: "array",
  items: requestSchema,
  minItems: 1
};

Проверка:

const validateBatch = ajv.compile(batchSchema);

const isValidBatch = validateBatch(batch);

if (!isValidBatch) {
  console.log(validateBatch.errors);
}

При обработке batch важно валидировать каждый элемент отдельно после общей проверки массива.


Разделение схем по методам и динамическая маршрутизация

При росте RPC API фиксированные схемы становятся неудобными. Используется динамическая регистрация:

const registry = new Map();

function registerMethod(name, schema) {
  registry.set(name, ajv.compile(schema));
}

function validateRpc(method, params) {
  const validator = registry.get(method);

  if (!validator) {
    return { valid: false, error: "Method not found" };
  }

  const valid = validator(params);

  return { valid, errors: validator.errors };
}

Такой подход позволяет масштабировать RPC без изменения ядра валидации.


Использование строгого режима Ajv

Ajv поддерживает строгую проверку схем, которая полезна для RPC:

const ajv = new Ajv({
  allErrors: true,
  strict: true
});

Строгий режим выявляет:

  • неиспользуемые свойства схем
  • ошибки в описании типов
  • некорректные ключевые слова

Это снижает вероятность логических ошибок в контракте RPC.


Форматирование ошибок в RPC через Ajv

Ajv возвращает массив ошибок, который требует преобразования в RPC-формат:

function formatAjvErrors(errors) {
  return errors.map(err => ({
    path: err.instancePath,
    message: err.message,
    keyword: err.keyword
  }));
}

Пример интеграции:

if (!valid) {
  return {
    jsonrpc: "2.0",
    error: {
      code: -32602,
      message: "Invalid params",
      data: formatAjvErrors(validate.errors)
    },
    id
  };
}

Middleware-архитектура RPC с Ajv

В серверных приложениях часто используется промежуточный слой:

async function rpcMiddleware(req, res) {
  const { method, params, id } = req.body;

  const validation = validateRpc(method, params);

  if (!validation.valid) {
    res.json({
      jsonrpc: "2.0",
      error: {
        code: -32602,
        message: validation.error || "Invalid params",
        data: validation.errors
      },
      id
    });
    return;
  }

  const result = await handlers[method](params);

  res.json({
    jsonrpc: "2.0",
    result,
    id
  });
}

Такой слой отделяет бизнес-логику от проверки структуры данных.


Расширенные возможности схем в RPC

Ajv поддерживает сложные конструкции JSON Schema, которые полезны для RPC:

oneOf для перегруженных методов

const paramsSchema = {
  oneOf: [
    {
      type: "object",
      required: ["email"],
      properties: {
        email: { type: "string" }
      }
    },
    {
      type: "object",
      required: ["phone"],
      properties: {
        phone: { type: "string" }
      }
    }
  ]
};

dependentRequired для зависимых параметров

const schema = {
  type: "object",
  properties: {
    token: { type: "string" },
    refreshToken: { type: "string" }
  },
  dependentRequired: {
    refreshToken: ["token"]
  }
};

Композиция схем для масштабируемого RPC

При больших системах схемы разбиваются на модули:

const baseRequest = {
  type: "object",
  required: ["jsonrpc", "method", "id"],
  properties: {
    jsonrpc: { const: "2.0" },
    method: { type: "string" },
    id: {}
  }
};

const fullRequest = {
  allOf: [
    baseRequest,
    {
      properties: {
        params: { type: "object" }
      }
    }
  ]
};

Такой подход снижает дублирование и упрощает сопровождение RPC-протокола.


Поведение при неизвестных полях и защита контракта

В RPC важно ограничивать входные данные:

additionalProperties: false

Это предотвращает:

  • скрытые параметры
  • атаки через лишние поля
  • неконсистентность контрактов

В некоторых случаях допускается мягкий режим:

additionalProperties: true

но он снижает строгость API и усложняет контроль версий.


Производительность валидации RPC

Ajv компилирует схемы в функции, что делает проверку высокопроизводительной. Ключевые оптимизации:

  • предварительная компиляция схем
  • повторное использование валидаторов
  • минимизация количества схем на запрос

Типичный поток:

  1. регистрация схем при старте
  2. компиляция Ajv
  3. кеширование функций валидации
  4. выполнение проверки без повторной интерпретации схем

Интеграция с типизацией и контрактами

При использовании TypeScript схема Ajv часто становится источником правды:

  • генерация типов из JSON Schema
  • синхронизация RPC контрактов
  • единая модель данных для клиента и сервера

Это позволяет поддерживать строгий контракт между сторонами RPC без дублирования логики валидации.