Стандарты и RFC: язык нормативных документов
Типичная сцена. В pull request спорят двое. Один: «в RFC сказано SHOULD, значит рекомендация,
можно не делать». Второй: «SHOULD — это “следует”, то есть надо». Через сорок минут команда
всё ещё спорит — не о протоколе, а о значении одного английского слова. Спор бессмысленный:
у этого слова есть письменное определение, принятое в 1997 году и действующее в тысячах
документов. Оно занимает две страницы, и тот, кто его прочитал, экономит команде эти сорок минут.
Эта глава не про грамматику. Она про узкий, жёстко формализованный диалект английского,
на котором написаны спецификации протоколов, стандарты языков, RFC, документы W3C и ISO,
а также внутренние design docs. Хорошая новость: словарь у диалекта маленький и закрытый,
конструкции повторяются, освоить его можно даже со слабым разговорным английским. Плохая:
читается он не так, как обычный текст, и «интуитивный» перевод слов should, may, can, will
регулярно превращается в баги в проде. Предыдущая глава —
Чтение документации — разбирала язык туториалов и README;
здесь другой жанр: документация объясняет и уговаривает, спецификация ограничивает.
Про базовый уровень языка, без которого трек не поможет, честно сказано в
карте трека.
Когда это нужно по работе, а не «для развития»: реализуете протокол или формат — HTTP, OAuth,
JWT, WebSocket — и поведение в углу спецификации не такое, как вы предположили; спорите с чужой
поддержкой о том, кто нарушил стандарт; аудитор ссылается на пункт, который надо прочитать;
пишете API-контракт для трёх команд из разных стран; ищете в Security Considerations
готовую модель угроз от авторов протокола. Всё это работа с текстом, а не с речью:
нужен не беглый разговорный английский, а умение медленно и точно прочитать двадцать предложений.
Нормативный текст не объясняет, а обязывает
Сравните два предложения об одном и том же.
Документация:
You'll usually want to set a timeout here, otherwise a slow server can hang
your worker for minutes.
Спецификация:
A client MUST NOT wait indefinitely for a response. A client SHOULD apply
a timeout to each request.
Первое обращается к вам (you), объясняет причину (otherwise), допускает исключения (usually).
Второе не обращается ни к кому лично: субъект — роль (a client), глагол — ключевое слово,
причина не указана вовсе. Нормативный текст не убеждает, он задаёт условие: реализация либо
соответствует ему, либо нет. Практическое следствие: значимо каждое слово, и особенно — подлежащее.
A recipient MUST ignore... и A sender MUST NOT send... — два разных требования к двум разным
сторонам, и их постоянно путают, читая по диагонали. Первый вопрос к любому предложению:
кто обязан, и мы ли это?
BCP 14: одиннадцать слов, на которых держится половина интернета
Ключевые слова определены в RFC 2119 («Key words for use in RFCs to Indicate Requirement Levels», 1997) с уточнением из RFC 8174 (2017). Вместе они образуют BCP 14 — ссылку на неё вы увидите почти в любом RFC.
| Слово | Синонимы | Нормативный смысл | Как это читает носитель |
|---|---|---|---|
MUST |
REQUIRED, SHALL |
абсолютное требование | «без этого реализация неверна, точка» |
MUST NOT |
SHALL NOT |
абсолютный запрет | «нельзя, исключений нет» |
SHOULD |
RECOMMENDED |
могут быть веские причины поступить иначе, но последствия надо полностью понять и взвесить | «делай так, если не можешь объяснить, почему нет» |
SHOULD NOT |
NOT RECOMMENDED |
то же самое, наоборот | «так почти всегда не надо, и это надо обосновать» |
MAY |
OPTIONAL |
полностью на усмотрение реализации | «хочешь — делай; а вот партнёр обязан работать в обоих случаях» |
Три вещи здесь важнее самих определений.
SHOULD — это не «можно забить». RFC 2119 формулирует жёстко: могут существовать
valid reasons in particular circumstances поступить иначе, но полные последствия должны быть
understood and carefully weighed. На языке практики: отклонение от SHOULD — архитектурное
решение, которое надо записать, ровно для этого существует
ADR.
«Мы не делаем X, потому что Y» — нормально. «Мы не делаем X, потому что там же should» — нет.
MAY — это не про вас, а про партнёра. Самая недооценённая строка таблицы.
Если спецификация говорит, что сервер MAY не прислать заголовок, ваш клиент обязан
корректно работать без него: MAY всегда означает парное скрытое требование к другой стороне.
Регистр решает. До 2017 года было неясно, что делать с the client should retry строчными.
RFC 8174 закрыл вопрос: слово нормативно, только когда написано заглавными.
Отсюда абзац, который вы видели сотни раз:
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
"SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this
document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174]
when, and only when, they appear in all capitals, as shown here.
when, and only when — юридическая формула «тогда и только тогда».
are to be interpreted, а не must be interpreted: оборот is to be адресует предписание
читателю документа, а не реализации. Встретив строчное should, проверьте, есть ли в документе
этот абзац: если есть, строчное should — обычное английское слово из авторского пояснения.
Как читать требование
Проверь раздел Conventions"] B -- "да" --> S["Найди подлежащее: client, server,
sender, recipient, user agent"] S --> T{"Это наша роль?"} T -- "нет" --> U["Требование к партнёру.
Спроси: что мы обязаны пережить,
если он поступит так или иначе"] T -- "да" --> D{"Какое слово?"} D -- "MUST / SHALL / REQUIRED" --> E["Обязательно.
Нарушение = несоответствие"] D -- "MUST NOT / SHALL NOT" --> F["Запрещено. Исключений нет"] D -- "SHOULD / SHOULD NOT" --> G{"Есть веская причина отклониться?"} D -- "MAY / OPTIONAL" --> H["На усмотрение.
Оба варианта законны"] G -- "нет" --> I["Делай как написано"] G -- "есть" --> J["Взвесь последствия
и запиши решение в ADR"]
Что НЕ является ключевым словом
Здесь половина ошибок при чтении и почти все ошибки при письме.
can— способность или физическая возможность, не разрешение.The server can return 503описывает, что бывает, а не что позволено. Хотели разрешить — пишитеMAY.will— предсказание, не требование.The token will expire in 3600 seconds— факт.mustстрочными — по RFC 8174 не нормативно, но в текстах ISO всё иначе, см. ниже.is required to,needs to,has to— разговорные обороты вне закрытого словаря, как и двусмысленноеit is recommended that: этоRECOMMENDEDили мнение автора?
Не только IETF: ISO, W3C, ECMA
Ловушка для того, кто выучил BCP 14 и решил, что теперь всё понимает: в других организациях словарь другой.
| IETF (RFC) | ISO / IEC | W3C | |
|---|---|---|---|
| требование | MUST, SHALL, REQUIRED |
shall строчными |
MUST |
| рекомендация | SHOULD, RECOMMENDED |
should |
SHOULD |
| разрешение | MAY, OPTIONAL |
may |
MAY |
| возможность | can |
can |
can |
слово must |
не нормативно | запрещено к использованию | обычно нормативно |
В ISO/IEC Directives, Part 2 — своде правил
написания стандартов ISO — сказано прямо: требование выражается словом shall, а must
использовать нельзя, потому что его резервируют за внешними обязательствами, которые стандарт
не устанавливает (законы, физика). Вывод: прежде чем спорить о слове, посмотрите, чей документ.
Строчное shall в ISO — железное требование, хотя в RFC оно бы не значило ничего.
Третий диалект — спецификации языков программирования, где нормативность выражена
терминами-категориями. Классика — стандарт C
(ISO/IEC 9899, доступен черновик N1570):
undefined behavior — никаких требований вообще, компилятор вправе делать что угодно;
unspecified behavior — несколько допустимых вариантов, документировать выбор не обязаны;
implementation-defined behavior — вариантов несколько, но свой реализация обязана
задокументировать; locale-specific behavior — зависит от национальных настроек.
По-английски четыре термина выглядят похоже; по смыслу это лестница от «полного произвола»
до «обязанности объясниться», и на ней держатся многолетние споры об оптимизациях компилятора.
В спецификации ECMAScript (tc39.es/ecma262) нормативность выражена
иначе — пошаговыми алгоритмами: Let x be ..., If ..., then, Perform ...,
Throw a TypeError exception. Императив Let там значит не «пусть будет»,
а «на этом шаге движок обязан сделать так».
Анатомия RFC: что читать первым
RFC не читают подряд. У документа жёсткая структура, заданная RFC 7322, и по ней навигируются так же, как по коду.
Шапка — метаданные, решающие, стоит ли читать дальше:
Obsoletes: 2616— документ заменяет перечисленные, их читать не нужно; на странице старого RFC будет обратное,Obsoleted by: .... Классика: HTTP/1.1 описывался в RFC 2616, потом был разбит на RFC 7230–7235, а сейчас действуют RFC 9110 (семантика), 9111 (кэширование), 9112 (HTTP/1.1). Половина статей в интернете до сих пор цитирует 2616. Про сам протокол — HTTP.Updates: 3864— дополняет, не отменяя; опаснееObsoletes, потому что для полной картины нужны оба документа одновременно.Category—Standards Track,Informational,Experimental,Best Current Practice.Informationalне обязывает ни к чему: это может быть описание чужого проприетарного протокола, и ссылаться на такой RFC как на стандарт — типичная ошибка в спорах.
Дальше по разделам: Abstract (4–8 самодостаточных предложений, читается всегда) →
Introduction (контекст, можно пропустить) → Conventions / Terminology →
основной текст → Security Considerations → IANA Considerations → References.
Три из них важнее прочих. Conventions / Terminology — здесь включается BCP 14 и определяются
термины: если документ вводит resource owner или user agent, дальше эти слова значат ровно
написанное, а не то, что вы думаете; читать до основного текста. Security Considerations —
обязательный для всех RFC раздел (RFC 3552),
готовая модель угроз от авторов протокола и часто самая полезная для практика часть; ср.
Моделирование угроз.
References делятся на Normative — обязательные к прочтению и реализации —
и Informative, то есть контекст и историю.
Два внешних инструмента экономят часы: datatracker.ietf.org показывает переписку рабочей группы, где видно, почему формулировка именно такая, а errata — исправления к опубликованным RFC. Текст RFC после публикации не меняется никогда, поэтому известные ошибки живут там: если предложение выглядит противоречиво, проверьте errata до спора с коллегой.
Практический смысл диаграммы: Internet-Draft — это не стандарт. Ссылка вида
draft-ietf-something-07 — черновик, который через полгода станет другим или исчезнет.
Код, опирающийся на draft, — техдолг с известной датой; так и напишите в комментарии.
Грамматика нормативного английского
Конструкций мало, и они повторяются.
Пассив без агента. The request is rejected — кем? В хорошей спецификации такого нет,
в средней — сплошь и рядом. Читая, достраивайте субъект; когда пишете сами — не используйте
пассив в требованиях вообще. Роли вместо людей: подлежащее — a client, the server,
a sender, a recipient, an implementation, a user agent, но никогда you и we.
Артикль значим: A client MUST... — любой клиент, The client MUST... — тот самый,
о котором шла речь выше.
Условия. Четыре разных слова, которые русскоязычные читают одинаково:
If the header field is present, the recipient MUST validate it.
→ условие; если поля нет, требование не применяется
Unless otherwise specified, the default value is 30 seconds.
→ «если явно не указано иное»; готовая формула, встречается постоянно
When a connection is closed, pending requests MUST be failed.
→ момент времени, а не гипотеза: это точно случится
Where the two definitions conflict, this document takes precedence.
→ «в тех случаях, в которых»; не про место, а про случаи
Последнее — частая ошибка чтения: where в нормативном тексте почти никогда не значит «где».
Область действия отрицания. Самая опасная конструкция:
A client MUST NOT retry the request unless the response includes a Retry-After
header field.
По умолчанию повторять запрещено; разрешение появляется только при наличии заголовка. Русскоязычный читатель часто переворачивает логику и запоминает «надо повторять, если есть Retry-After» — а это другое утверждение. Приём: разбейте на два предложения, «нельзя вообще» плюс «исключение».
Формулы, которые стоит узнавать в лицо: for the purposes of this document — дальше идёт
определение, действующее только здесь; unless otherwise noted — «если не оговорено иное»;
as defined in Section 4.2 of [RFC9110] — нормативная отсылка, читать обязательно;
is said to be — вводит термин; takes precedence over — что важнее при конфликте правил;
at its discretion — сигнал уровня MAY. И принцип робастности из RFC 793 (TCP), раздел 2.10:
be conservative in what you do, be liberal in what you accept from others. Отсюда во всех
протоколах формула A recipient MUST ignore unrecognized parameters — получатель обязан молча
проигнорировать незнакомое. Это готовый ответ на вопрос «а что если придёт лишнее поле».
Грамматика формата: ABNF
Синтаксис в IETF-документах задаётся не прозой, а нотацией ABNF (RFC 5234). Читать её проще, чем кажется:
credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
auth-scheme = token
token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" ) *"="
; [ ... ] — необязательная часть
; 1*SP — один или более пробелов: «1*» значит «минимум один»
; / — альтернатива, «или»; *"=" — ноль или более знаков равенства
Три обозначения покрывают 90% случаев: * — повторение (2*4DIGIT — от двух до четырёх цифр),
[ ] — опциональность, / — выбор. Спор о том, допустим ли пробел в заголовке,
ABNF решает за десять секунд — быстрее, чем чтение прозы вокруг.
Из текста в протокол
Полезное упражнение — выписать требования на диаграмму и посмотреть, кто кому что должен (формулировки ниже учебные, в стиле RFC 6749, а не процитированы дословно).
with the authorization server AUTH-->>C: 200 OK + access token Note left of AUTH: The authorization server MUST NOT issue
a token to an unauthenticated client C->>RES: GET /orders + Authorization header Note right of C: Clients SHOULD send the token
in the Authorization header field RES->>RES: проверка подписи, exp, aud Note right of RES: The resource server MUST validate
the token before granting access RES-->>C: 401 Unauthorized + WWW-Authenticate Note left of RES: The response MAY include
an error description parameter
Что видно на картинке и плохо видно в тексте: SHOULD на шаге 5 означает, что бывают клиенты,
присылающие токен иначе, и сервер ресурсов обязан это учитывать; MAY на последнем шаге означает,
что клиент обязан обработать ответ без описания ошибки — здесь ломается большинство самописных
SDK; требование на шаге 7 адресовано серверу ресурсов, а не серверу авторизации, и путаница ролей
в OAuth стоит дороже всего: OAuth 2.0 и OIDC, JWT и токены.
Как писать собственный нормативный текст
Внутренний RFC, design doc, описание API-контракта, раздел «Требования» в задаче — всё это нормативные тексты, даже если вы их так не называете. И читают их люди, для которых английский тоже неродной: любую двусмысленность прочитают в двух смыслах, и оба реализуют. Правило, решающее 80% проблем: одно требование — одно предложение — один субъект — одно ключевое слово.
Ниже — формулировки, которые постоянно встречаются в текстах русскоязычных инженеров. Колонка «как читает носитель» здесь главная: проблема почти всегда не в грамматике, а в том, что смысл смещается.
| Что написано | Как это читает носитель | Как переписать |
|---|---|---|
The client should send the Authorization header. |
«желательно; вообще-то можно и без него» | The client MUST send an Authorization header field with every request. |
It is necessary to validate the signature. |
«кому-то надо бы проверить»; субъекта нет | The resource server MUST validate the token signature. |
The token will be checked by the server. |
описание будущего, не требование | The server MUST verify the token before processing the request. |
Server can reject the request. |
«сервер в принципе на это способен» — не разрешение | The server MAY reject the request with 429 Too Many Requests. |
Don't use this endpoint. |
разговорный тон, для спеки — шум | Clients MUST NOT use this endpoint. It is retained for backward compatibility only. |
It is not recommended to store tokens in localStorage. |
личное мнение автора | Storing access tokens in browser local storage is NOT RECOMMENDED. |
The field is required, otherwise error 400 will be returned. |
смешаны требование и последствие | The request MUST include an idempotency_key field. + A request without this field MUST be rejected with 400 Bad Request. |
In case of error the client must retry. |
«error» — какая? must строчными не нормативно |
If the server responds with 429 or 503, the client SHOULD retry using exponential backoff. |
We recommend to use pagination. |
грамматически неверно и не нормативно | Clients SHOULD paginate requests that may return more than 100 items. |
Правила, которые редакторы стандартов применяют механически:
- Никаких
etc.,and so onиand/orв требованиях. Список либо исчерпывающий, либо это не требование; вместоand/orпишитеA, B, or both. - Не используйте
MUSTбез проверяемого критерия.The code MUST be readable— пожелание. - Не ставьте
MUSTради важности. RFC 2119 отдельно предупреждает: императивы применять экономно и только там, где это нужно для совместимости. Документ, где всёMUST, невозможно внедрить постепенно. - Пронумеруйте требования.
R-14: The service MUST ...— тогда на них можно ссылаться в тестах, задачах и разговоре. Про связку с приёмкой — Критерии приёмки и Документирование требований. - Отделите нормативное от пояснительного пометкой
Note:илиThis section is non-normative.
Скелет внутреннего документа, который работает в командах:
## RFC-014: Idempotency keys for the payments API
Status: Draft | Accepted | Superseded by RFC-021
### Terminology
The key words MUST, MUST NOT, SHOULD, and MAY in this document are to be
interpreted as described in BCP 14 when they appear in all capitals.
An **idempotency key** is a client-generated string of at most 64 characters.
### Requirements
R-1. A client MUST include an Idempotency-Key header field in every POST /payments request.
R-2. The server MUST store the key for at least 24 hours.
R-3. On a repeated request with the same key and the same body, the server MUST return
the original response and MUST NOT create a second payment.
R-4. On a repeated request with the same key and a different body, the server MUST
respond with 422 Unprocessable Content.
R-5. A client SHOULD generate keys as UUIDv4.
Note: 24 hours matches the retry window of our payment provider.
Раздел Terminology здесь не бюрократия: он превращает SHOULD из вежливого пожелания
в проверяемое условие, на которое можно сослаться в ревью, не начиная спор с нуля. Ту же технику —
нумерованные требования, одно предложение на требование — стоит переносить в описания задач
(Задачи и баг-репорты) и изменений
(Коммиты и pull request).
Как спорить, ссылаясь на спецификацию
Ссылка на стандарт — самый сильный аргумент в технической переписке и самый лёгкий способ прозвучать высокомерно. Разница в формулировке.
Плохо: This is wrong, read the RFC.
→ снисходительно, читается как «ты некомпетентен»
Плохо: I think GET should not change data, no?
→ это не мнение, а требование стандарта; неуверенность здесь вредит
Хорошо: Per RFC 9110 Section 9.3.1, a GET request is expected to be safe, and this
handler writes to the DB — a proxy or a crawler could duplicate the write.
Could we move it to POST, or is there a constraint I'm missing?
Три приёма делают ссылку рабочей, а не показной: указывайте раздел, а не только номер
документа (ссылка на весь RFC читается как «иди разбирайся сам»); переводите требование
в последствие — не «стандарт запрещает», а «прокси повторит запрос, и мы получим двойное
списание»; оставляйте место для контраргумента — иногда команда сознательно отклонилась
от SHOULD, и это записано в ADR. Тон несогласия разбирается в
Комментариях на ревью.
Типичные ошибки
Читать спецификацию подряд. RFC на 200 страниц не предназначен для линейного чтения. Рабочий порядок: Abstract → Terminology → оглавление → нужный раздел → Security Considerations. Пропустив раздел терминов, вы подставите бытовые значения слов и получите неверную модель протокола.
Читать устаревший документ. Проверяйте шапку на Obsoleted by: поисковик выдаёт старые RFC
чаще новых, потому что на них больше ссылок.
Путать роли. sender/recipient, client/server, authorization server/resource server.
Требование, адресованное не вам, — подсказка, к чему готовиться, а не задача.
Считать SHOULD необязательным, а MAY — обязательным. Ошибки зеркальные и обе дорогие:
первая ломает совместимость, вторая раздувает объём работы.
Переводить «должен» как should. Главная ловушка русскоязычного инженера: в русском «должен» —
обязанность, в английском should — рекомендация; обязанность — это MUST (или shall в ISO).
Другие расхождения такого рода собраны в
Типичных ошибках русскоязычных инженеров.
Писать спецификацию красиво. Синонимы — враг нормативного текста. Если сущность называется
access token, она называется так во всех предложениях; the token, the credential, the key —
это уже три разные сущности для читателя. Подробнее — Ясность.
Мини-практикум
Для каждого предложения определите: требование, рекомендация, разрешение или не норма? И к кому оно?
1. A server MUST NOT generate a Content-Length header field in a 204 response.
2. Implementations should be careful when parsing user input.
3. The client MAY include an Accept-Language header field.
4. Where this document conflicts with [RFC7231], this document takes precedence.
5. Tokens can be revoked at any time.
6. A recipient MUST ignore any parameter it does not recognize.
Разбор:
- Запрет, адресован серверу. Заглавные — нормативно.
- Не норма:
shouldстрочными, действия нет. Комментарий автора, реализовать нельзя. - Разрешение клиенту — и скрытое требование к серверу работать без этого заголовка.
- Не требование к реализации, а правило разрешения конфликтов между документами: смотрите на
Where. - Не норма:
can— описание возможности. Разрешение выглядело бы какMAY be revoked, обязанность поддержать отзыв — какMUST support revocation. - Требование получателю, тот самый принцип робастности: добавление нового поля в API не ломает корректных клиентов.
Чеклист
Когда читаете: проверил шапку на Obsoleted by и Category; прочитал раздел терминов до основного
текста; по каждому требованию определил роль-субъект; отличил ключевые слова заглавными от «слов,
похожих на требования»; заглянул в errata, если текст противоречив; прочитал
Security Considerations до того, как счёл реализацию готовой.
Когда пишете: включил абзац про BCP 14; у каждого требования есть явный субъект и ровно одно
ключевое слово; требования пронумерованы и проверяемы; пояснения помечены как Note:;
нет etc., and/or и синонимов для одной сущности; MUST стоит только там, где нарушение
действительно ломает совместимость.
Мини-итог
Нормативный английский — это диалект из нескольких десятков конструкций и одиннадцати ключевых слов,
значения которых определены письменно. Его не надо чувствовать, его надо знать. Разница между
MUST, SHOULD и MAY не стилистическая: это разница между «сломается интеграция»,
«нужно записать решение в ADR» и «партнёр обязан пережить оба варианта». Регистр букв имеет силу,
а should — не перевод слова «должен». Умение читать такие тексты даёт инженеру редкую позицию
в споре: не «я так думаю», а «вот раздел, вот формулировка, вот следствие». Умение их писать
превращает design doc в документ, который нельзя понять двумя способами.
Источники
- RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels и RFC 8174 — Ambiguity of Uppercase vs Lowercase; вместе — BCP 14
- RFC 7322 — RFC Style Guide и RFC 2026 — The Internet Standards Process
- RFC 5234 — Augmented BNF for Syntax Specifications: ABNF
- RFC 3552 — Guidelines for Writing RFC Text on Security Considerations
- RFC 9110 — HTTP Semantics — образец современного нормативного текста
- IETF Datatracker, RFC Errata и The Tao of the IETF — как устроен процесс изнутри
- ISO/IEC Directives, Part 2 — правила написания стандартов ISO
- ECMAScript Language Specification — алгоритмы по шагам; ISO/IEC 9899 draft N1570 — undefined, unspecified, implementation-defined
Что дальше
Сообщения об ошибках и логи: как читать чужие и писать свои — следующий жанр инженерного английского. Спецификация задаёт, что должно произойти; сообщение об ошибке говорит, что пошло не так. Оба текста читают в состоянии стресса, и оба должны пониматься однозначно с первого раза.