Перейти к содержанию

Стоимость и пределы запроса

Пакетная загрузка уменьшает повторное получение авторов, но не ограничивает любой документ клиента. Корректный запрос может содержать множество псевдонимов, повторных фрагментов и глубоких связей. Поэтому добавим предварительный бюджет AST: максимальную глубину и число полей после раскрытия используемых фрагментов. Это учебная ресурсная политика, а не полноценная оценка стоимости большой промышленной схемы. У каждого ограничения должен быть понятный смысл и известная граница действия.

Транспорт уже ограничивает число байтов тела и длину строки query. Эти пределы не исчезают. Новый шаг работает позже: после разбора языка и проверки документа по схеме, но до предметного выполнения. Короткий документ иногда способен развернуть большой граф через фрагменты, а длинная строка комментариев не обязательно означает дорогую операцию. Размер текста и стоимость выполнения — разные величины, поэтому один предел не заменяет все остальные.

Сначала корректный документ

Функция inspectDocument вызывает parse с maxTokens, затем validate по текущей схеме. Если язык содержит синтаксическую ошибку, неизвестное поле или цикл фрагментов, возвращается ошибка до нашего обхода. Такая последовательность важна для безопасности алгоритма: мы не должны самостоятельно следовать бесконечному циклу, который стандартная проверка уже умеет отклонить. Проверка бюджета дополняет правила языка, а не переписывает весь механизм валидации.

Затем выбирается конкретная операция через getOperationAST и operationName. Если документ содержит несколько операций без однозначного выбора, бюджет не должен случайно оценивать первую. Выполнение и предварительная оценка обязаны обсуждать одно намерение клиента. Иначе дешёвая операция может пройти проверку, а сервер исполнит другую, дорогую. Такое несоответствие часто возникает, когда защитный анализ живёт отдельно от реального пути исполнения.

function inspectDocument(source,operationName){
 let doc;try{doc=parse(source,{maxTokens:2000})}catch(error){return [error]}
 const errors=validate(schema,doc);if(errors.length)return errors;
 const operation=getOperationAST(doc,operationName);
 if(!operation)return [new GraphQLError("Specify a known operation",{extensions:{code:"BAD_REQUEST"}})];
 const fragments=new Map(doc.definitions.filter(d=>d.kind===Kind.FRAGMENT_DEFINITION).map(d=>[d.name.value,d]));
 let count=0,maxDepth=0;
 function visit(set,depth){maxDepth=Math.max(maxDepth,depth);if(maxDepth>6||count>80)return;for(const field of set.selections){
  if(count>80||maxDepth>6)return;
  if(field.kind===Kind.FIELD){count++;if(field.selectionSet)visit(field.selectionSet,depth+1)}
  else if(field.kind===Kind.INLINE_FRAGMENT)visit(field.selectionSet,depth);
  else visit(fragments.get(field.name.value).selectionSet,depth);
 }}
 visit(operation.selectionSet,1);
 if(count>80||maxDepth>6)return [new GraphQLError("Query budget exceeded",{extensions:{code:"QUERY_LIMIT"}})];
 return [];
}

Листинг — фрагмент inspectDocument полного academy.mjs состояния lesson-10. Наши пределы: глубина не больше шести, число выбранных полей не больше восьмидесяти. Обход прекращается сразу после превышения, а не продолжает раскрывать огромный документ ради точного итогового числа. Даже анализ стоимости сам должен иметь предел работы. Иначе защита становится ещё одним способом занять процесс до основного выполнения.

Фрагменты и глубина

Обычное поле увеличивает число, а его вложенная выборка повышает глубину. Inline-фрагмент меняет условие типа, но сам не добавляет уровень предметного объекта. Именованный фрагмент раскрывает свой selectionSet на той же глубине. Это соответствует форме графа, а не числу текстовых строк документа. Если считать только узлы AST без раскрытия, многократно используемый фрагмент может выглядеть дешёвым, хотя его поля повторяются в нескольких местах ответа.

Стандартная validate запрещает циклы фрагментов, однако ациклическое переиспользование всё равно может сильно расширить обход. Поэтому ранняя остановка по count и maxDepth находится внутри рекурсии. Ограничение maxTokens также уменьшает размер разбираемого входа. Эти решения дают понятный учебный анализ. Для схемы с более сложными директивами или новыми вариантами языка необходимо отдельно проверить, какие конструкции поддерживает выбранный анализатор.

Мы считаем поля консервативно и не пытаемся доказать, что условная директива skip обязательно уберёт ветку при данных variables. Такой подход может отвергнуть документ, который реально выполнился бы дешевле, зато договор проще и не зависит от хитрой оценки каждого условия. Если продукту нужна точная стоимость, следует явно расширить алгоритм и согласовать его с coercion переменных, а не считать текущую функцию универсально точной.

Количество полей и размер списка

Счётчик полей не знает, сколько элементов вернёт resolver списка. В нашей Академии courses фиксирован тремя объектами, а courseConnection ограничена first от одного до двадцати. Это удерживает текущую выдачу в небольшом диапазоне. Если источник станет реальной базой с тысячами карточек, старое поле courses превратится в обход ресурсной границы. Тогда потребуется ограниченный договор, миграция клиентов или отдельный предел, а не только увеличение count.

Более развитая стоимость учитывает множители списков, конкретные аргументы и цену источника. Например, один author может обращаться к удалённому сервису, а соседний title читается из памяти. Глубина этих полей одинаково небольшая, но цена различна. Наш AST-бюджет не выдаёт миллисекунды и не обещает точный расход. Он задаёт верхнюю границу структуры документа и служит понятной частью более широкой защиты.

Даже допустимый документ может выполняться медленно из-за неисправности источника. Поэтому остаются таймауты, квоты одновременных операций и управление отменой. Node-сервер учебного курса не содержит распределённого ограничения запросов по пользователю и не предотвращает все виды нагрузки. Публичное приложение потребовало бы отдельного анализа транспорта и источников. Правильнее указать конкретные свойства реализации, чем назвать одну функцию полной защитой GraphQL.

Отказ до resolver

Если превышен бюджет, run возвращает errors с QUERY_LIMIT до вызова graphql. Предметные функции чтения и mutation не должны начинать действие. Это важный наблюдаемый договор: отказ политики происходит до сохранения Enrollment, а не после изменения с сообщением «слишком дорого». Для настоящих систем это особенно существенно, когда один документ содержит и поля результата, и побочные эффекты. Предварительное решение должно относиться к целой выбранной операции.

Попробуйте самостоятельно построить документ с большим числом разных псевдонимов course. Даже если каждый запрашивает существующий ID, число полей может превысить предел. Исправление — меньшая связная выборка либо согласованное расширение политики, а не удаление псевдонимов в ответе после исполнения. Ограничение должно быть объяснимым клиенту: QUERY_LIMIT отличается от неизвестного поля, отсутствия студента и ошибки метаданных автора.

При подготовке урока обход, запросы и проверки не запускались; текст описывает ожидаемое поведение по исходникам. Полное состояние сохраняет частичные ошибки author и демонстрационную идентификацию. Следующий шаг разберёт развитие схемы: добавим displayTitle, оставим title для прежних документов и обозначим устаревшее поле без мгновенного удаления. Совместимость API тоже является частью качества, которую нельзя заменить ресурсным пределом.

Псевдоним изменяет имя результата, но каждое выбранное поле по-прежнему участвует в бюджете. Сто вариантов copyN:course не становятся одной дешёвой карточкой только из-за одинакового ID. Запросный кэш может сократить чтение автора, однако ответ всё равно содержит повторные структуры. Поэтому анализ не сводит разные псевдонимы к одному ключу источника. Он рассматривает форму запрашиваемой выдачи отдельно от оптимизации получения исходных объектов.

Исходники урока

Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-10. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.

Первичные источники: документация 1.