rewrite 2026-09 and 2027 articles for reader-facing quality
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 22:26:56 +03:00
parent 3bfc3f21c4
commit 440c8721dc
69 changed files with 4424 additions and 3533 deletions
+280 -136
View File
@@ -1,168 +1,312 @@
function escapeHtml(value) { return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;'); }
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + escapeHtml(alt) + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((cell) => '<th scope="col">' + cell + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function cloneFixed(value) { return JSON.parse(JSON.stringify(value)); }
function deepFreeze(value) { if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.values(value).forEach(deepFreeze); Object.freeze(value); } return value; }
function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
function bodyText(html) { return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
function cloneFixed(value) {
return JSON.parse(JSON.stringify(value));
}
function deepFreeze(value) {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.values(value).forEach(deepFreeze);
Object.freeze(value);
}
return value;
}
function plainText(html) {
return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim();
}
function bodyText(html) {
return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
const REFERENCES = deepFreeze({
rfc9110: { title: 'RFC 9110 — HTTP Semantics', url: 'https://www.rfc-editor.org/rfc/rfc9110', version: 'RFC 9110, June 2022, DOI 10.17487/RFC9110' },
rfc9112: { title: 'RFC 9112 — HTTP/1.1', url: 'https://www.rfc-editor.org/rfc/rfc9112', version: 'RFC 9112, June 2022, DOI 10.17487/RFC9112' },
rfc9114: { title: 'RFC 9114 — HTTP/3', url: 'https://www.rfc-editor.org/rfc/rfc9114', version: 'RFC 9114, June 2022, DOI 10.17487/RFC9114' },
rfc8446: { title: 'RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3', url: 'https://www.rfc-editor.org/rfc/rfc8446', version: 'RFC 8446, August 2018, DOI 10.17487/RFC8446' },
http: { title: 'RFC 9110 — HTTP Semantics', version: 'IETF Standards Track, June 2022', url: 'https://www.rfc-editor.org/rfc/rfc9110.html' },
tls: { title: 'RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3', version: 'IETF Standards Track, August 2018', url: 'https://www.rfc-editor.org/rfc/rfc8446.html' },
});
function sources(entries) { return '<ul>' + entries.map(({ key, use, boundary }) => { const ref = REFERENCES[key]; return '<li><a href="' + ref.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(ref.title) + '</a> — ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>'; }).join('') + '</ul>'; }
function sources(entries) {
return '<ul>' + entries.map(({ key, use, boundary }) => {
const ref = REFERENCES[key];
return '<li><a href="' + ref.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(ref.title) + '</a> — ' + escapeHtml(ref.version) + '. ' + escapeHtml(use) + ' Граница применимости: ' + escapeHtml(boundary) + '</li>';
}).join('') + '</ul>';
}
const FIXED_HTTP_TLS_CASES = deepFreeze({
'planned-http-tls-question-v1': { id: 'planned-http-tls-question-v1', planDate: '2027-05', sourceCutoff: '2026-07-31', protocolQuestion: { id: 'named-http-tls-boundary-question', version: 'named-protocol-version-question', statement: 'synthetic-only-no-endpoint-or-client' }, evidence: { state: 'not-collected', strength: 'input-bounded', command: 'not-run', endpoint: 'not-collected', clientConfig: 'not-collected', trace: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Fixed in-memory planning literal. No HTTP request, TLS handshake, socket, endpoint, client configuration, certificate, command, trace, error, clock, file, environment, secret, telemetry, production system or customer data is read, created, changed or inferred.' },
'undated-plan-or-cutoff-v1': { id: 'undated-plan-or-cutoff-v1', planDate: '', sourceCutoff: '2026-07-31', protocolQuestion: { id: 'named-http-tls-boundary-question', version: 'named-protocol-version-question', statement: 'synthetic-only-no-endpoint-or-client' }, evidence: { state: 'not-collected', strength: 'input-bounded', command: 'not-run', endpoint: 'not-collected', clientConfig: 'not-collected', trace: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'unnamed-protocol-or-version-question-v1': { id: 'unnamed-protocol-or-version-question-v1', planDate: '2027-05', sourceCutoff: '2026-07-31', protocolQuestion: { id: '', version: '', statement: 'synthetic-only-no-endpoint-or-client' }, evidence: { state: 'not-collected', strength: 'input-bounded', command: 'not-run', endpoint: 'not-collected', clientConfig: 'not-collected', trace: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'evidence-stronger-than-input-v1': { id: 'evidence-stronger-than-input-v1', planDate: '2027-05', sourceCutoff: '2026-07-31', protocolQuestion: { id: 'named-http-tls-boundary-question', version: 'named-protocol-version-question', statement: 'synthetic-only-no-endpoint-or-client' }, evidence: { state: 'claimed', strength: 'stronger-than-input', command: 'not-run', endpoint: 'not-collected', clientConfig: 'not-collected', trace: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'hidden-command-endpoint-or-config-v1': { id: 'hidden-command-endpoint-or-config-v1', planDate: '2027-05', sourceCutoff: '2026-07-31', protocolQuestion: { id: 'named-http-tls-boundary-question', version: 'named-protocol-version-question', statement: 'synthetic-only-no-endpoint-or-client' }, evidence: { state: 'not-collected', strength: 'input-bounded', command: 'hidden', endpoint: 'not-collected', clientConfig: 'not-collected', trace: 'not-collected' }, requestedConclusion: 'synthetic-plan-hand-off', boundary: 'Negative fixed literal only.' },
'disallowed-positive-network-conclusion-v1': { id: 'disallowed-positive-network-conclusion-v1', planDate: '2027-05', sourceCutoff: '2026-07-31', protocolQuestion: { id: 'named-http-tls-boundary-question', version: 'named-protocol-version-question', statement: 'synthetic-only-no-endpoint-or-client' }, evidence: { state: 'not-collected', strength: 'input-bounded', command: 'not-run', endpoint: 'not-collected', clientConfig: 'not-collected', trace: 'not-collected' }, requestedConclusion: 'handshake-succeeded', boundary: 'Negative fixed literal only.' },
status404: { status: 404, headers: { 'content-type': 'text/plain' }, body: 'missing' },
status503: { status: 503, headers: { 'retry-after': '2' }, body: 'busy' },
tlsNameMismatch: { error: 'ERR_TLS_CERT_ALTNAME_INVALID', stage: 'certificate' },
invalid: { status: 0, headers: {}, body: '' },
});
export function createFixedHttpTlsCase(id = 'planned-http-tls-question-v1') { const value = FIXED_HTTP_TLS_CASES[id]; return value ? deepFreeze(cloneFixed(value)) : undefined; }
function stop(status, reason, nextAction) { return deepFreeze({ status, reason, nextAction, productionEffect: 'not-attempted' }); }
export function createFixedHttpTlsCase(id = 'status404') {
return FIXED_HTTP_TLS_CASES[id] ? deepFreeze(cloneFixed(FIXED_HTTP_TLS_CASES[id])) : undefined;
}
export function assessFixedHttpTlsPlan(input) {
if (!Object.values(FIXED_HTTP_TLS_CASES).some((item) => JSON.stringify(item) === JSON.stringify(input))) return stop('stop-unknown-fixed-input', 'input-is-not-a-known-named-fixed-literal', 'select-a-named-fixed-case');
if (input.planDate !== '2027-05' || input.sourceCutoff !== '2026-07-31') return stop('stop-undated-plan-or-cutoff', 'may-2027-plan-and-july-2026-cutoff-are-required', 'name-2027-05-and-2026-07-31');
if (!input.protocolQuestion?.id || !input.protocolQuestion?.version || !input.protocolQuestion?.statement) return stop('stop-unnamed-protocol-or-version-question', 'protocol-and-version-question-must-be-named-synthetic-literals', 'name-the-protocol-version-question');
if (input.evidence?.state !== 'not-collected' || input.evidence?.strength !== 'input-bounded') return stop('stop-evidence-stronger-than-input', 'no-evidence-may-be-claimed-or-stronger-than-the-fixed-input', 'keep-evidence-not-collected-and-input-bounded');
if (input.evidence?.command !== 'not-run' || input.evidence?.endpoint !== 'not-collected' || input.evidence?.clientConfig !== 'not-collected' || input.evidence?.trace !== 'not-collected') return stop('stop-hidden-command-endpoint-or-config', 'commands-endpoints-client-config-and-traces-must-remain-not-run-or-not-collected', 'record-only-not-run-state-limits');
if (input.requestedConclusion !== 'synthetic-plan-hand-off') return stop('stop-disallowed-positive-network-conclusion', 'a-handshake-http-or-production-positive-conclusion-is-forbidden', 'use-synthetic-plan-hand-off');
return deepFreeze({ status: 'synthetic-plan-hand-off', caseId: input.id, protocolQuestion: deepFreeze(cloneFixed(input.protocolQuestion)), evidence: deepFreeze(cloneFixed(input.evidence)), boundary: input.boundary, productionEffect: 'not-attempted', nextAction: 'give-the-question-and-state-limits-to-a-future-evidence-owner' });
if (!input || typeof input !== 'object') return { status: 'stop', class: 'invalid-input', action: 'проверить форму наблюдения' };
if (input.error === 'ERR_TLS_CERT_ALTNAME_INVALID') return { status: 'check-certificate-name', class: 'TLS', action: 'сверить имя узла с SAN сертификата' };
if (input.status === 404) return { status: 'check-route', class: 'HTTP', action: 'проверить URI и маршрут' };
if (input.status === 503) return { status: 'check-overload', class: 'HTTP', action: 'прочитать Retry-After и проверить зависимость' };
if (input.status >= 200 && input.status < 400) return { status: 'success', class: 'HTTP', action: 'проверить тело и контракт ответа' };
return { status: 'stop', class: 'unknown', action: 'собрать расширенный вывод отдельно' };
}
export function runFixedHttpTlsFixture() {
const checks = [
['status404', 'check-route'],
['status503', 'check-overload'],
['tlsNameMismatch', 'check-certificate-name'],
['invalid', 'stop'],
].map(([id, expected]) => ({ id, expected, actual: assessFixedHttpTlsPlan(createFixedHttpTlsCase(id)).status }));
return deepFreeze({ passed: checks.filter((item) => item.expected === item.actual).length, total: checks.length, accepted: checks.every((item) => item.expected === item.actual), checks });
}
function revision(meta, parts, referenceEntries) {
const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries);
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength);
return deepFreeze({ ...meta, contentHtml, proseLength });
}
export function runFixedHttpTlsFixture() { const expected = [['planned-http-tls-question-v1', 'synthetic-plan-hand-off'], ['undated-plan-or-cutoff-v1', 'stop-undated-plan-or-cutoff'], ['unnamed-protocol-or-version-question-v1', 'stop-unnamed-protocol-or-version-question'], ['evidence-stronger-than-input-v1', 'stop-evidence-stronger-than-input'], ['hidden-command-endpoint-or-config-v1', 'stop-hidden-command-endpoint-or-config'], ['disallowed-positive-network-conclusion-v1', 'stop-disallowed-positive-network-conclusion']]; const checks = expected.map(([id, status]) => ({ id, expected: status, actual: assessFixedHttpTlsPlan(createFixedHttpTlsCase(id)).status })); const sample = createFixedHttpTlsCase(); return deepFreeze({ passed: checks.filter((item) => item.expected === item.actual).length, total: checks.length, accepted: checks.every((item) => item.expected === item.actual) && Object.isFrozen(sample) && Object.isFrozen(sample.protocolQuestion) && Object.isFrozen(sample.evidence), checks: deepFreeze(checks) }); }
function revision(meta, parts, referenceEntries) { const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries); const proseLength = bodyText(contentHtml).length; if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength); return deepFreeze({ ...meta, contentHtml, proseLength }); }
const refs = [
{ key: 'rfc9110', use: 'Определяет семантические части HTTP сообщения и границы интерпретации поля.', boundary: 'Не подтверждает ответ конкретного origin, proxy, клиента, endpoint или операции.' },
{ key: 'rfc9112', use: 'Даёт первичную спецификацию framing HTTP/1.1, опубликованную до cutoff.', boundary: 'Не доказывает, какой wire-format или ошибка будут у неназванного соединения.' },
{ key: 'rfc9114', use: 'Фиксирует модель HTTP/3 и её привязку к QUIC как справочный механизм.', boundary: 'Не делает утверждений о доступности HTTP/3 в каком-либо клиенте или сервисе.' },
{ key: 'rfc8446', use: 'Даёт первичную спецификацию TLS 1.3 и сообщений handshake.', boundary: 'Не заменяет certificate chain, policy, trace, negotiated parameters или результат handshake.' },
{ key: 'http', use: 'Нужен для различения методов, статусов, заголовков, маршрутизации и ответа посредника.', boundary: 'Не объясняет конкретную конфигурацию прокси, DNS или причину ошибки в вашем сервисе.' },
{ key: 'tls', use: 'Нужен для разбора рукопожатия TLS 1.3, проверки имени узла и сообщения об ошибке сертификата.', boundary: 'Не подтверждает доверие к конкретному центру сертификации и не заменяет проверку ключевого материала.' },
];
const practice = revision({ slug: 'editorial-2027-05-practice-http-tls-guide', title: 'Полевой справочник HTTP и TLS: практическая карта вопроса', categories: ['Инженерные практики', 'Сети'], cover: '/assets/editorial/2027/http-tls-guide-2027-handshake-header-map.svg', excerpt: 'План на май 2027: как поставить HTTP/TLS-вопрос без подмены его сетевым результатом.', readingMinutes: 19 }, [
p('Самая дорогая ошибка при разборе HTTP/TLS — начать с команды и закончить чужим выводом: «сервер ответил», «сертификат плохой» или «TLS работает». Без названного протокольного вопроса эти фразы склеивают уровни: HTTP semantics, message framing, TLS negotiation, DNS, маршрут и политика клиента. Цена — часы в неверной команде и ложное исправление, которое меняет не ту границу. В полевом справочнике сначала должна появиться карта вопроса, а не инструмент в терминале.'),
p('Редакторская дата — 2026-07-31; май 2027 ещё не наступил. Поэтому P111 не содержит controlled handshake, HTTP trace, cURL/OpenSSL run, endpoint, client config, certificate, error или production result. Это строго plan/scenario на <code>2027-05</code> с source cutoff <code>2026-07-31</code>. Единственный положительный outcome — <code>synthetic-plan-hand-off</code>; у него всегда <code>productionEffect: not-attempted</code>. Любая приятная фраза о соединении здесь была бы выдуманным сетевым отчётом.'),
h2('Карта начинается с вопроса, а не с симптома'),
p('Практический вопрос должен иметь два имени: что различаем и для какой версии это различение формулируем. В fixed literal это <code>named-http-tls-boundary-question</code> и <code>named-protocol-version-question</code>. Это не hostname, не URI, не ALPN value и не название библиотеки. Такие placeholders нарочно достаточно бедны: они не позволяют незаметно привезти endpoint, команду или configuration под видом «контекста». Если protocol/version question не назван, модель останавливается; default вроде «наверное, HTTPS» уже является неподтверждённой подстановкой.'),
p('Вопрос удобно делить на четыре плоскости. Семантика HTTP отвечает, что означает method, target, status и field в сообщении. Framing отвечает, где сообщение начинается и заканчивается в выбранном HTTP mapping. TLS handshake отвечает, какие сообщения и параметры могут быть согласованы до прикладного обмена. Доставка и policy отвечают за то, что находится вокруг этого: имена, маршрутизация, trust store, ограничения клиента и промежуточные узлы. Отсутствие наблюдения в одной плоскости не делает фактом вывод о другой.'),
figure('/assets/editorial/2027/http-tls-guide-2027-handshake-header-map.svg', 'Карта вопроса HTTP и TLS: отдельные блоки HTTP-семантики, framing, TLS handshake и клиентской policy сходятся только в synthetic hand-off; красные блоки запрещают endpoint, команду и вывод о соединении.', 'Схема помогает назвать границу будущего вопроса. Это не trace, не handshake и не результат обращения к сети.'),
table('Версионированная карта вопроса на 2027-05', ['Плоскость', 'Что можно назвать сейчас', 'Что не известно', 'Что передать owner'], [
['HTTP semantics', 'method/status/header question', 'конкретное сообщение и ответ', 'правило интерпретации'],
['HTTP framing', 'named HTTP version question', 'байты, порядок и длины', 'какой mapping требуется уточнить'],
['TLS handshake', 'named TLS version question', 'negotiation и certificate', 'какой class evidence нужен'],
['Client policy', 'boundary question', 'trust, proxy, retry, config', 'полномочия и provenance'],
['Итог', 'synthetic plan', 'endpoint, trace, error, result', 'hand-off без network action'],
const practice = revision({
slug: 'editorial-2027-05-practice-http-tls-guide',
title: 'HTTP и TLS без гадания: как разобрать 404, 503 и ошибку сертификата',
categories: ['Сети', 'Диагностика'],
cover: '/assets/editorial/2027/http-tls-guide-2027-handshake-header-map.svg',
excerpt: 'Практический маршрут от текста ошибки к уровню, на котором действительно нужно искать причину.',
readingMinutes: 14,
}, [
p('Запрос к сервису может не дойти до приложения, хотя пользователь видит обычную страницу ошибки. <code>404</code> говорит о выбранном ресурсе, <code>503</code> — о доступности обработчика, а <code>ERR_TLS_CERT_ALTNAME_INVALID</code> возникает ещё до HTTP. Цена смешения этих уровней — часы на исправление маршрута в коде, когда проблема находится в имени узла или на обратном прокси.'),
p('Разберём три наблюдаемых случая на одном маршруте: сначала получим ответ локального HTTP-сервера, затем отделим HTTP-статус от TLS-этапа и зафиксируем следующий запрос для проверки. Пример учебный: он не обращается к внешней сети и не выдаёт локальный результат за состояние чужой инфраструктуры.'),
h2('Сначала фиксируем точку отказа'),
p('У любого запроса есть последовательность: разрешение имени, установка TCP-соединения, TLS-рукопожатие для <code>https</code>, отправка HTTP-запроса и чтение ответа. Важен первый наблюдаемый факт. Если клиент не смог проверить сертификат, у него нет HTTP-статуса. Если получен <code>404</code>, TLS и HTTP-соединение уже состоялись, а искать нужно URI, метод или маршрутизацию.'),
table('Какой уровень проверять первым', ['Наблюдение', 'Уровень', 'Первое действие', 'Чего не делать'], [
['Ошибка имени сертификата', 'TLS', 'сверить host и SAN', 'не менять HTTP-заголовки'],
['404 Not Found', 'HTTP-маршрут', 'проверить путь и метод', 'не увеличивать timeout'],
['503 Service Unavailable', 'обработчик или зависимость', 'прочитать заголовки и логи', 'не повторять POST вслепую'],
['Нет ответа и timeout', 'сеть или сервер', 'разделить connect/read timeout', 'не считать это 500'],
]),
figure('/assets/editorial/2027/http-tls-guide-2027-handshake-header-map.svg', 'Схема уровней запроса: DNS, TCP, TLS, HTTP и ответ с точкой остановки диагностики.', 'Диаграмма показывает порядок уровней. Стрелка останавливается на первом слое, о котором есть наблюдение.'),
h2('Учебный HTTP-ответ на локальном сервере'),
p('Чтобы не спорить о сообщении браузера, поднимем два endpoint в одном Node-процессе. Вход — путь запроса. Ожидаемый результат — числовой статус и тело. Такой запуск показывает семантику HTTP-ответа, но не тестирует TLS: для TLS нужен отдельный сервер с сертификатом и проверкой имени.'),
code([
"import { createServer } from 'node:http';",
'',
"const server = createServer((request, response) => {",
" if (request.url === '/health') {",
" response.writeHead(200, { 'content-type': 'text/plain' });",
" response.end('ok');",
' return;',
' }',
" response.writeHead(404, { 'content-type': 'text/plain' });",
" response.end('missing');",
'});',
'',
"server.listen({ port: 0, host: '127.0.0.1' }, async () => {",
' const port = server.address().port;',
" const response = await fetch('http://127.0.0.1:' + port + '/missing');",
" console.log(response.status, await response.text());",
' server.close();',
'});',
].join('\n')),
p('Запустите файл командой <code>node check-http.mjs</code>. В консоли будет <code>404 missing</code>. Входом является только локальный путь; ожидаемый результат проверяем двумя значениями. Если изменить путь на <code>/health</code>, получится <code>200 ok</code>. Это полезнее, чем проверять только цвет страницы: статус и тело принадлежат разным частям HTTP-контракта.'),
h2('Почему 503 нельзя лечить повтором по умолчанию'),
p('<code>503</code> означает, что сервер временно не может обработать запрос. Заголовок <code>Retry-After</code> может дать клиенту ориентир, но он не делает повтор безопасным. Для <code>GET</code> повтор обычно не меняет ресурс, а для <code>POST</code> повтор способен создать вторую запись или списать деньги повторно. Перед автоматикой нужно знать семантику метода и наличие ключа идемпотентности.'),
p('В диагностической записи сохраняйте метод, путь без секретов, статус, важные заголовки, время ожидания и первый байт ответа. Не прикладывайте токен авторизации и полные cookie. Для <code>503</code> сначала проверяется зависимость, ограничение соединений или обслуживание сервера; затем выбирается контролируемый повтор с лимитом и задержкой.'),
h2('Ошибка сертификата находится до HTTP'),
p('TLS 1.3 устанавливает защищённый канал и проверяет имя узла в сертификате. Имя берётся из URL и должно совпасть с одним из значений Subject Alternative Name. Если клиент подключается к IP вместо доменного имени, использует устаревший alias или получает сертификат другого виртуального хоста, HTTP-заголовок <code>Host</code> проблему не исправит: запрос ещё не отправлен.'),
p('Практическая граница видна в подробном выводе клиента: после строки об успешной проверке сертификата и до ответа <code>404</code> проблема уже относится к HTTP-маршруту. Если TLS завершается исключением, статус и тело приложения искать бессмысленно. Это простое разделение экономит одну итерацию проверки.'),
p('Проверка должна разделять имя, цепочку доверия и срок действия. В учебной диагностике достаточно зафиксировать hostname, адрес назначения и текст ошибки библиотеки. Флаг вроде <code>--insecure</code> может подтвердить, что сервер отвечает, но он отключает проверку и не является исправлением. После такого эксперимента соединение нужно закрыть и повторить проверку с обычной валидацией.'),
h2('Порядок диагностики'),
ol([
'Записать URL, метод, момент запроса и безопасный идентификатор запроса; убрать Authorization, Cookie и персональные параметры.',
'Проверить разрешение имени и адрес назначения отдельно от приложения.',
'Для HTTPS проверить hostname, SAN, срок действия и цепочку сертификата обычным клиентом.',
'Только после успешного TLS посмотреть HTTP-статус, заголовок Allow, Location, Retry-After и тело.',
'Сопоставить метод и действие: повтор разрешать только для операции с понятной идемпотентностью.',
'Зафиксировать один следующий тест и ожидаемый результат, например «/health возвращает 200, /missing — 404».',
]),
h2('Заголовок не является trace'),
p('Поле заголовка имеет имя, значение и определённую семантику в контексте сообщения; это не означает, что мы видели такое поле на wire. RFC 9110 полезен для разделения semantics от конкретной реализации: он помогает сформулировать, что будущий вопрос относится к request, response или representation. Но даже точное правило спецификации не сообщит, какое поле добавит intermediary, что нормализует библиотека и в какой момент приложение увидит значение. Поэтому нельзя писать «этот header был причиной» без материала, который разрешён и сохранён в новом scope.'),
p('То же относится к HTTP/1.1 и HTTP/3. RFC 9112 описывает HTTP/1.1 framing, RFC 9114 — HTTP/3 поверх QUIC. Из этого следует, что название версии меняет допустимый механизм разбора; не следует, что выбранный клиент использовал один из них. Практический справочник не выбирает fallback, не предлагает «универсальный» request и не рисует bytes. Пока нет именованной версии и authorised evidence, правильная инструкция короче: назвать вопрос и не выдавать формат за наблюдение.'),
h2('TLS — отдельный слой причинности'),
p('TLS 1.3 описан в RFC 8446 как последовательность протокольных сообщений и проверок, но текст RFC не превращается в report о неизвестном peer. Слова certificate, handshake failure и negotiated version особенно опасны: они звучат технически, однако без certificate chain, configuration и trace дают только предположение. Сценарий P111 не хранит и не получает эти объекты. Он не называет algorithm, cipher suite, SNI, ALPN, CA store или параметр команды, потому что все они были бы не нейтральными примерами, а скрытой client configuration.'),
p('Плановая граница полезна именно своей строгостью. Будущий owner может решить, что вопрос на самом деле про HTTP semantics и TLS-данные не нужны; может решить обратное; может остановить discovery из-за отсутствия полномочий. Нельзя компенсировать эту неопределённость «типичной» настройкой cURL или OpenSSL. Поведение инструментов меняется по release и build, а P111 не фиксирует проверенный exact release source для такого поведения. В перечне будущих средств остаётся только состояние: <code>command: not-run</code>, <code>trace: not-collected</code>.'),
h2('Безопасный runnable literal'),
code("import { createFixedHttpTlsCase, assessFixedHttpTlsPlan } from './upgrade-2027-05.mjs';\n\nconst plan = createFixedHttpTlsCase('planned-http-tls-question-v1');\nconst handOff = assessFixedHttpTlsPlan(plan);\nconsole.log({ status: handOff.status, effect: handOff.productionEffect });\n// { status: 'synthetic-plan-hand-off', effect: 'not-attempted' }"),
p('Пример запускаем, но он не является сетевой проверкой. Factory делает JSON clone named fixed literal и затем recursive deep freeze; evaluator принимает только JSON-равный known literal и fail-closed закрывает всё остальное. Вызов не использует socket, fetch, child process, file, environment, clock, certificate store или telemetry. Он демонстрирует исключительно редакционный контракт: исходные данные ограничены literal, следовательно evidence не может стать сильнее input и итог не может превратиться в handshake result.'),
h2('Порядок действий для будущего владельца'),
ol(['Сначала зафиксировать <code>planDate: 2027-05</code> и <code>sourceCutoff: 2026-07-31</code>; недатированный сценарий закрыть.', 'Назвать protocol question и version question, не подменяя их endpoint, URL, hostname или именем клиента.', 'Разнести desired inference по HTTP semantics, framing, TLS и client policy; не делать один симптом общей причиной.', 'Оставить cURL/OpenSSL и любые traces только в списке <code>not-run/not-collected</code>, без команды и конфигурации.', 'Проверить fixed literal; evidence stronger than input, hidden command/endpoint/config и positive network conclusion должны вернуть stop.', 'Передать question contract будущему evidence owner, который отдельно решит полномочия, минимальный набор данных и метод.' ]),
h2('Как не потерять версию в формулировке'),
p('Версия в карте — не число для украшения. Она отвечает на вопрос, какой именно нормативный vocabulary допустимо использовать в будущем contract. HTTP/1.1 framing нельзя тихо подменить общим словом «HTTP», а TLS 1.3 нельзя описать как абстрактное «шифрование». При этом названная версия не становится observed negotiation. Два утверждения живут на разных уровнях: первое — о границе формулировки, второе — о факте конкретного взаимодействия. План имеет право только на первое.'),
p('Полезный приём для owner — выписать рядом «вопрос версии» и «наблюдение версии», не смешивая поля. У первого допустимы synthetic labels и ссылка на RFC, у второго потребовались бы собственные provenance, timestamp, collection policy и rule интерпретации. Если эти столбцы оказались одинаковыми, вероятно, в draft уже проникли данные. В P111 такого перехода нет: version остаётся именем будущего различения, а не свойством клиента или peer.'),
p('Это различение экономит усилие при эскалации. Вместо спора, «почему не проверить прямо сейчас», hand-off показывает недостающие полномочия: target не назван, client context не согласован, сохранение output не определено. Тогда решение может быть не только запуском. Команда вправе сузить вопрос, перенести его в другой контур или признать, что стандартного знания достаточно. Справочник не навязывает проверку как обязательный ритуал; он не даёт ритуалу замаскироваться под результат.'),
p('Нельзя и переносить на будущее сегодняшнюю форму вопроса как неизменную. К маю 2027 могут измениться продуктовая цель, ownership, допустимый канал наблюдения или сам смысл риска. Cutoff фиксирует только то, какие источники использованы для редакционного vocabulary; он не замораживает реальную систему. Поэтому hand-off не содержит заранее выбранного client или target и не обещает, что будущая работа обязана следовать тем же шагам. Его ценность — передать точку честного старта, а не создать скрытый проектный план.'),
p('Практический reader может использовать карту уже сейчас как редакционную проверку вопроса. Достаточно спросить: где в тексте граница HTTP, где framing, что относится к TLS, кто определяет client policy, и какое слово создаёт наблюдение без источника. Если на один из вопросов нет ответа, это не повод добавить знакомую техническую деталь. Это повод вернуть карточку к именованному placeholder. В таком виде она остаётся применимой к будущему scope и не расходует доверие к документации.'),
p('Так карта остаётся рабочей и после того, как будущий scope изменит инструменты или вовсе откажется от их использования: она описывает не команду, а порядок различения.'),
h2('Ограничения и следующий шаг'),
p('Этот текст не обучает обходить TLS-проверки, не задаёт client flags и не рекомендует отключать валидацию. Он также не диагностирует 4xx/5xx, reset, timeout, mismatch или любую другую ошибку: таких error здесь нет. RFC-источники до cutoff дают язык для точного будущего вопроса, но не дают ответ о конкретной сети. Даже существование method или handshake message в стандарте не подтверждает, что оно применимо к неназванной системе.'),
p('Следующий шаг — сохранить hand-off как ограниченную карточку до отдельного authorised scope. В новом artefact будущий owner сможет определить, какой evidence допустим, как отделить observation от inference и какие данные нельзя сохранять. До этого нельзя превратить вопрос в endpoint, client config или production action. Здесь завершён только безопасный переход: вопрос имеет дату, cutoff, границу и следующую роль; production effect не пытались получить.'),
p('Локальный сервер не показывает работу CDN, DNS-балансировщика, корпоративного proxy или реального центра сертификации. Статус 404 не доказывает, что маршрут одинаково настроен во всех регионах, а 503 не называет виновную зависимость. Для этого нужны согласованные логи и сетевые наблюдения с разрешённым доступом.'),
p('Следующим шагом соберите две безопасные записи: успешный запрос к health-endpoint и один ошибочный запрос с тем же hostname. Сравните этап, статус, заголовки и время. Если различие появляется до HTTP, оставайтесь на TLS или сети; если оба запроса дошли до сервера, переходите к маршруту и контракту приложения.'),
], refs);
const mechanism = revision({ slug: 'editorial-2027-05-mechanism-http-tls-guide', title: 'Полевой справочник HTTP и TLS: границы inference', categories: ['Инженерные практики', 'Сети'], cover: '/assets/editorial/2027/http-tls-guide-2027-symptom-boundary-matrix.svg', excerpt: 'План на май 2027: как отличить классы HTTP/TLS-ошибок от выводов, которых evidence не поддерживает.', readingMinutes: 20 }, [
p('Дорогая диагностическая ошибка — объявить один наблюдаемый симптом причиной на соседнем уровне: HTTP status назвать TLS отказом, ошибку проверки certificate — свойством заголовка, а отсутствие response — доказательством сервера. Такая склейка заставляет менять policy, proxy или приложение вслепую; цена — длинный цикл повторной работы и риск затронуть не ту границу. Механизм нужен не для «быстрого диагноза», а для точного запрета на inference, который не следует из входа.'),
p('На 2026-07-31 у P111 нет наблюдаемого симптома, HTTP trace, controlled handshake, endpoint, client config, certificate, command output или production result; май 2027 ещё впереди. Поэтому это plan/scenario на <code>2027-05</code>, cutoff <code>2026-07-31</code>. Мы разбираем классы различения, а не фактические errors. Positive outcome только <code>synthetic-plan-hand-off</code> с <code>productionEffect: not-attempted</code>; «успешно», «исправлено» и «соединение установлено» запрещены.'),
h2('Inference имеет потолок'),
p('Evidence не должен быть сильнее input. Если input — named question и state <code>not-collected</code>, допустимый output — только вопрос с ограничениями. Он не может вдруг содержать negotiated protocol, header set, certificate finding или error classification. Это не избыточная осторожность: поздний reader не видит, где закончился вход и началась уверенная интерпретация. В validator case <code>evidence-stronger-than-input-v1</code> специально заявляет claimed evidence и закрывается с <code>stop-evidence-stronger-than-input</code>.'),
p('Ошибка класса и ошибка механизма — не одно и то же. «HTTP semantic mismatch» описывает расхождение между ожидаемой и допустимой интерпретацией сообщения. «HTTP framing boundary» относится к способу выделить сообщение в конкретном mapping. «TLS negotiation boundary» относится к протокольному согласованию до HTTP semantics. «Client trust/policy boundary» относится к тому, что клиент разрешает использовать. Эти labels полезны как future buckets, но без конкретного input ни один bucket не заполнен и ни один не является диагнозом.'),
figure('/assets/editorial/2027/http-tls-guide-2027-symptom-boundary-matrix.svg', 'Матрица границ inference: HTTP-семантика, framing, TLS negotiation и client policy сопоставлены с допустимым доказательством; красная колонка запрещает вывод о соседнем уровне и positive conclusion.', 'Матрица различает классы вопросов и не сообщает об ошибке реального клиента, сервера или сертификата.'),
table('Граница между симптомом и выводом', ['Класс будущего вопроса', 'Минимальное evidence', 'Что ещё не следует', 'Безопасное действие сейчас'], [
['HTTP semantics', 'явно разрешённый message context', 'TLS причина или endpoint state', 'сформулировать semantic rule'],
['HTTP framing', 'versioned wire mapping', 'значение header или policy', 'назвать HTTP version question'],
['TLS negotiation', 'разрешённый handshake material', 'HTTP status и app behaviour', 'назвать TLS version question'],
['Trust / client policy', 'явная client configuration', 'peer identity или network fault', 'отделить policy от peer'],
['P111 сейчас', 'fixed literal only', 'любой error/result', 'synthetic hand-off'],
const mechanism = revision({
slug: 'editorial-2027-05-mechanism-http-tls-guide',
title: 'Где рождается ошибка HTTP: разбираем цепочку DNS, TLS и заголовков',
categories: ['Сети', 'HTTP'],
cover: '/assets/editorial/2027/http-tls-guide-2027-symptom-boundary-matrix.svg',
excerpt: 'Механика запроса по слоям: почему одинаковое слово «ошибка» требует разных проверок.',
readingMinutes: 15,
}, [
p('Когда браузер показывает «не удалось подключиться», виден только итог. Цена ошибки — изменить код приложения, не проверив, что запрос вообще не прошёл TLS, или принять ответ кэша за ответ origin-сервера. Для точной диагностики нужно восстановить цепочку по наблюдаемым границам, а не угадывать виновника по одному коду.'),
p('В этой статье разложим запрос на переходы и посмотрим, какие поля подтверждают каждый переход. Практический результат — короткая таблица: какой лог или команда отвечает на конкретный вопрос. Пример запускается локально на Node и показывает обмен HTTP-заголовками без обращения к внешнему узлу.'),
h2('Пять границ одного запроса'),
p('DNS превращает имя в адрес, TCP устанавливает поток байтов, TLS защищает его и связывает с именем, HTTP передаёт метод и путь, а приложение формирует ответ. Посредник может добавить свой статус или заголовок на каждом шаге. Поэтому поле <code>server</code> в ответе не доказывает, что ответ сформирован именно вашим приложением.'),
table('Поле и вопрос диагностики', ['Граница', 'Что можно утверждать', 'Что проверить'], [
['DNS', 'имя разрешилось в адрес', 'ответ A/AAAA и выбранный адрес'],
['TCP', 'порт принял соединение', 'connect error и время установки'],
['TLS', 'сертификат подходит имени', 'SAN, chain, protocol version'],
['HTTP', 'получен статус и заголовки', 'method, path, status, headers'],
['Приложение', 'обработан контракт endpoint', 'лог маршрута и request id'],
]),
figure('/assets/editorial/2027/http-tls-guide-2027-symptom-boundary-matrix.svg', 'Матрица симптомов HTTP и TLS с границей, которую подтверждает каждый вид наблюдения.', 'У каждой строки есть отдельный вопрос. Нельзя переносить ответ из соседней строки: HTTP-статус не подтверждает TLS, а DNS-ответ не подтверждает маршрут.'),
h2('Заголовок не равен доказательству источника'),
p('Заголовок <code>Via</code>, <code>Server</code> или пользовательский <code>X-Request-Id</code> помогает построить гипотезу, но это данные сообщения, а не криптографическая аттестация сервера. Посредник может удалить или переписать поля. Для сопоставления запроса с серверным логом нужен идентификатор, который генерируется на входе доверенного компонента и сохраняется без смены формата.'),
p('У HTTP/2 и HTTP/3 целевой authority может передаваться не так, как привычная строка <code>Host</code>. Поэтому в диагностической записи сохраняйте логическое имя назначения и не делайте вывод о виртуальном хосте по одному полю. Сопоставляйте его с тем, что использовал TLS-клиент.'),
p('Удобная минимальная запись выглядит так: метод, нормализованный путь, статус, request id, длительность, размер ответа и класс ошибки. Секреты и произвольные query-параметры не входят в журнал по умолчанию. Нормализация важна: если один компонент пишет полный URL, а другой — только путь, поиск по записи будет давать ложные пропуски.'),
h2('Учебный обмен с заголовками'),
p('Локальный сервер ниже возвращает два заголовка и JSON-тело. Вход — HTTP-запрос с <code>Accept</code>. Ожидаемый результат — <code>200</code>, тип содержимого и один идентификатор. Это реальный обмен между клиентом и сервером, но он не проверяет TLS или поведение прокси.'),
code([
"import { createServer } from 'node:http';",
'',
"const server = createServer((request, response) => {",
" response.writeHead(200, {",
" 'content-type': 'application/json; charset=utf-8',",
" 'x-request-id': 'local-001',",
' });',
" response.end(JSON.stringify({ method: request.method, path: request.url }));",
'});',
'',
"server.listen(0, 'localhost', async () => {",
' const port = server.address().port;',
" const endpoint = 'http://localhost:' + port + '/orders';",
' const response = await fetch(endpoint);',
" console.log(response.status, response.headers.get('x-request-id'));",
" console.log(await response.json());",
' server.close();',
'});',
].join('\n')),
p('Результат содержит <code>200 local-001</code> и объект с методом <code>GET</code> и путём <code>/orders</code>. Если убрать заголовок из ответа, клиент всё равно получит 200: это показывает, что request id — средство сопоставления, а не условие корректности HTTP. В рабочей системе нужно договориться, какой компонент отвечает за его создание и где он попадает в лог.'),
h2('TLS меняет порядок проверки'),
p('При HTTPS нельзя начинать с ответа приложения. Клиент сначала отправляет ClientHello, сервер выбирает параметры и предъявляет сертификат, затем стороны завершают рукопожатие. Проверка имени происходит относительно hostname, который клиент считает целевым. Если соединение идёт через proxy, отдельно фиксируйте имя proxy и имя origin: это две разные проверки.'),
p('Сертификат с правильной цепочкой, но неправильным SAN — ошибка имени. Сертификат с правильным SAN, который не доверен локальному хранилищу, — ошибка доверия. Сертификат с истёкшим сроком — ошибка времени. Эти причины нельзя объединять в «проблему SSL»: для каждой нужен собственный ожидаемый результат и свой владелец исправления.'),
h2('Кэш и промежуточный ответ'),
p('HTTP допускает intermediaries (посредников), а RFC 9110 отдельно описывает кэширование и маршрутизацию. Ответ может быть свежим объектом кэша, перенаправлением или сообщением origin. Проверяйте <code>Age</code>, <code>Cache-Control</code>, <code>ETag</code>, <code>Via</code> и время изменения тела, но не делайте вывод о кэше по одному заголовку: политика конкретного посредника может быть сложнее.'),
table('Набор безопасных полей для записи', ['Поле', 'Пример', 'Риск при сборе'], [
['method', 'GET', 'малый, если путь обезличен'],
['path', '/orders', 'query может содержать секрет'],
['status', '200', 'не показывает причину сам по себе'],
['durationMs', '42', 'нужно знать часы и границы замера'],
['requestId', 'local-001', 'нельзя принимать внешний id без правила'],
['responseSize', '31', 'не заменяет проверку тела'],
]),
h2('Порядок проверки по границам'),
ol([
'Зафиксировать имя, адрес и режим proxy отдельно; query-параметры очистить.',
'Проверить DNS и TCP независимым клиентом, сохранив только код ошибки и длительность.',
'Для HTTPS сверить hostname, SAN, цепочку и время действия сертификата.',
'Снять HTTP-статус и выбранные заголовки, затем сопоставить request id с логом доверенного входа.',
'Сравнить ответ с origin и кэшем, если между клиентом и приложением есть intermediary.',
'Записать причину только на том уровне, который подтверждён наблюдением.',
]),
h2('HTTP semantics не доказывает транспорт'),
p('RFC 9110 задаёт semantics message components. Он помогает проверить будущую формулировку вида «какое значение status или field допустимо интерпретировать», но не отвечает, достигло ли сообщение origin и не показывает путь. Даже если future owner имеет HTTP response, статус сам по себе не описывает TLS handshake, name resolution, retry policy или действия intermediary. Следовательно, нельзя начинать с слова «HTTP error» и сразу менять trust or transport assumptions; сначала нужно отделить реальный message context от гипотезы о причинах.'),
p('RFC 9112 нужен, когда сам вопрос относится к HTTP/1.1 message framing. Он не является доказательством bytes конкретной сессии и не говорит, что некоторый header «сломал соединение». RFC 9114 аналогично не превращает knowledge об HTTP/3 в evidence, что выбранный клиент договорился о QUIC. Версия должна быть частью именованного вопроса, иначе одинаковая терминология начинает скрывать разные механизмы. Именно поэтому unnamed protocol/version literal не получает guessed fallback, а возвращает stop.'),
h2('TLS handshake не является HTTP response'),
p('RFC 8446 фиксирует TLS 1.3 protocol. Он позволяет описать, какие классы данных вообще относятся к handshake, но не подтверждает ни один конкретный parameter. Certificate validation, server-name policy, local trust anchors, session resumption и application request остаются разными объектами. У future investigation может быть несколько независимых ограничений, однако P111 не называет их значений. Попытка вставить сюда «обычный» endpoint или client option сразу превратила бы механизм в скрытый experiment.'),
p('Различение классов полезно и при будущей ошибке. Например, сообщение приложения о невозможности получить response не разрешает назвать certificate причиной; certificate-related failure не разрешает приписать HTTP status; mismatch в semantics не говорит, что TLS настроен неверно. Это не отрицание причинных цепочек — это отказ сокращать цепочку без материалов. Будущий owner должен документировать, какое observation поддерживает какой inference и где inference заканчивается. P111 передаёт именно это правило, а не готовый troubleshooting recipe.'),
h2('Скрытая конфигурация делает ошибку неразличимой'),
p('Command, endpoint и client configuration — часть evidence method, а не декоративное приложение. Но в P111 они даже не собираются. Когда их значения скрыты, сторонний reader не знает, какая версия протокола была запрошена, какая policy влияла на ход, что мог изменить proxy и какая команда сформировала output. Это не приглашение опубликовать их здесь: правильное состояние — <code>command: not-run</code>, endpoint/config/trace <code>not-collected</code>. Case <code>hidden-command-endpoint-or-config-v1</code> fail-closed прекращает hand-off.'),
p('Отдельно запрещено выдумывать current behavior cURL или OpenSSL. У P111 нет exact pinned release material, который проверяет конкретную версию, build и option semantics; значит, текст не утверждает, как инструмент поведёт себя. Названия средств могут присутствовать только как очередь будущих средств, не как instruction. Такой предел спасает от самой коварной ошибки: читатель видит знакомую команду и принимает её как допустимую configuration для неизвестного production context.'),
h2('Runnable пример отказа'),
code("import { createFixedHttpTlsCase, assessFixedHttpTlsPlan } from './upgrade-2027-05.mjs';\n\nconst tooStrong = createFixedHttpTlsCase('evidence-stronger-than-input-v1');\nconst result = assessFixedHttpTlsPlan(tooStrong);\nconsole.log([result.status, result.productionEffect].join(' | '));\n// stop-evidence-stronger-than-input | not-attempted"),
p('В этом runnable fragment нет wire activity. Он берёт named in-memory literal, который JSON-clone factory отделила от caller и recursive deep freeze зафиксировала до оценки. Validator допускает только exactly known fixed inputs, поэтому произвольный object не может принести «наблюдение» вне списка. Вывод сообщает boundary модели, а не сетевую ошибку. Никакого cURL/OpenSSL subprocess, fetch, socket, certificate parser, file, environment или clock код не использует.'),
h2('Порядок будущего различения'),
ol(['Зафиксировать дату плана и cutoff; не называть будущую работу фактом.', 'Назвать protocol/version question и отделить HTTP semantics, framing, TLS и client policy.', 'Для каждого будущего observation заранее записать, какой inference он поддерживает, а какой нет.', 'Не принимать evidence, сильнее разрешённого input; claimed result в P111 всегда stop.', 'Не скрывать command, endpoint, client config или trace за пересказом; здесь все они not-run/not-collected.', 'Передать matrix будущему owner без diagnosis, remediation или positive network conclusion.' ]),
h2('Почему table не заменяет причинную запись'),
p('Матрица с четырьмя строками легко читается как классификатор, хотя ей нельзя быть классификатором. Она не получает строку лога и не возвращает label. Её задача уже: напомнить, что для каждого возможного утверждения нужна запись «вход → допустимое следствие → запрещённое следствие». Без среднего звена symptom превращается в вывод, а запрещённое следствие исчезает из обсуждения. Именно тогда привычные слова вроде «транспортная ошибка» начинают означать всё сразу.'),
p('В реальном authorised artefact причинная запись должна быть симметричной. Не только observation должен вести к hypothesis; hypothesis обязана перечислить, какой другой material мог бы её опровергнуть. Для HTTP semantics это может быть другой message context, для policy — иное правило клиента, для TLS — другой участок протокольных данных. P111 не перечисляет такие материалы по имени, чтобы не создать ложный collection plan. Он передаёт требование к форме будущей записи, не её заполненный экземпляр.'),
p('Особая польза этого подхода — отделить remediation от diagnosis. Даже верно определённый класс не предписывает, что надо менять: порядок исправления зависит от владельца, риска и contract вокруг системы. Поэтому phrase «значит, надо отключить проверку» здесь была бы одновременно неверным inference и опасной рекомендацией. Механизм останавливается раньше: он требует показать связь между разрешёнными входами и утверждением, после чего другой scope сможет обсуждать действие.'),
p('Граница useful и для коммуникации между ролями. Автор приложения может сформулировать expected semantics, security owner — правила доверия, platform owner — ограничения маршрута, а investigator — минимальный evidence contract. Ни одна роль не должна заполнять чужой столбец догадкой ради короткого ответа. В P111 эти роли не назначаются и не существует реального incident; matrix лишь показывает, что их утверждения имеют различный тип. Это снижает риск, что техническое слово будет принято за право сделать production change.'),
h2('Ограничения и следующий шаг'),
p('Матрица не заменяет capture, security review, protocol implementation review или incident process. Она не классифицирует настоящие errors, не назначает владельца certificate и не выбирает точку наблюдения. Даже при появлении данных классы могут пересекаться: один application symptom способен иметь несколько независимых причин. Механизм полезен до сбора тем, что запрещает писать результат заранее и заставляет определить доказательную границу.'),
p('Следующий шаг — будущему owner создать отдельный scope с разрешённым input, version pins, redaction rules и явным цепочечным описанием observation → inference. Этот новый scope может использовать другие инструменты, но он не является продолжением P111 по умолчанию. До такого решения единственная корректная публикация — карта границ и synthetic hand-off. Она не делает TLS или HTTP проще; она делает честным место, где заканчивается знание.'),
p('Локальный обмен не показывает потери пакетов, балансировку, корпоративный proxy и особенности браузерного хранилища. Поле <code>x-request-id</code> в учебном сервере задано вручную; в настоящем сервисе его происхождение и доверенная зона должны быть описаны отдельно. Не смешивайте диагностическую метку с секретом или пользовательским идентификатором.'),
p('Следующий шаг — добавить к одной безопасной проверке два вывода: сетевой этап и серверный лог. Сначала докажите, что запрос достиг доверенного входа, потом связывайте его с handler. Такой порядок уменьшает область поиска и не заставляет приложение отвечать за сбой, произошедший раньше.'),
], refs);
const field = revision({ slug: 'editorial-2027-05-field-http-tls-guide', title: 'Полевой справочник HTTP и TLS: передача synthetic evidence', categories: ['Инженерные практики', 'Сети'], cover: '/assets/editorial/2027/http-tls-guide-2027-evidence-handoff-loop.svg', excerpt: 'План на май 2027: hand-off вопроса об HTTP/TLS без поддельного сетевого отчёта.', readingMinutes: 18 }, [
p('Самая дорогая ошибка в полевой передаче — назвать вопрос отчётом: написать, что handshake проверен, header найден или ошибка воспроизведена, хотя получатель видит лишь пересказ. Такой артефакт быстро становится «фактом» в очереди, влияет на владельцев и может запустить ненужное изменение policy. Цена выше одной неверной заметки: provenance теряется, а будущая проверка вынуждена спорить с несуществующим evidence вместо того, чтобы начать с ясной постановки.'),
p('P111 не является network report. На редакторскую дату 2026-07-31 май 2027 ещё не наступил; это plan/scenario на <code>2027-05</code> с source cutoff <code>2026-07-31</code>. Здесь не было endpoint, certificate, client config, cURL/OpenSSL run, HTTP trace, controlled handshake, production result или error. Field focus означает только synthetic evidence hand-off. Разрешённый positive outcome — <code>synthetic-plan-hand-off</code>, а <code>productionEffect: not-attempted</code> остаётся на любом пути.'),
h2('Передавать вопрос, состояние и предел'),
p('Хорошая synthetic карточка содержит не «что случилось», а что ещё нельзя утверждать. В ней есть planDate, sourceCutoff, named protocol/version question, evidence state и next action. Поле <code>state: not-collected</code> не значит «лог потеряли» и не намекает на частную проверку; оно означает, что P111 ничего не собирал. Поле <code>strength: input-bounded</code> не измеряет качество будущих данных, а запрещает hand-off стать богаче собственных literals. Это небольшой контракт, зато reader может проверить каждую его границу.'),
p('Предел особенно важен для команд и traces. Слова «попробуйте cURL», «снимите OpenSSL handshake» обычно выглядят безобидно, но в реальной сети они уже задают endpoint, client policy, traffic и способ хранения output. В этом выпуске tools помещены только в очередь будущего решения: command <code>not-run</code>, endpoint/clientConfig/trace <code>not-collected</code>. Мы не даём command line, не описываем certificate, не придумываем error и не сообщаем, что чей-то сервер принимает или отклоняет соединение.'),
figure('/assets/editorial/2027/http-tls-guide-2027-evidence-handoff-loop.svg', 'Петля передачи synthetic evidence: дата и named protocol/version question проходят fail-closed gates для evidence, command, endpoint, config и positive conclusion; только затем вопрос передаётся будущему owner.', 'Петля изображает in-memory validation и не является журналом сети, HTTP trace или результатом TLS handshake.'),
table('Контракт synthetic HTTP/TLS hand-off', ['Поле', 'Состояние в P111', 'Что получает owner', 'Чего это не доказывает'], [
['Время', '2027-05 / cutoff 2026-07-31', 'редакционную границу', 'что работа в мае выполнена'],
['Вопрос', 'named synthetic protocol/version', 'объект уточнения', 'protocol choice или peer'],
['Evidence', 'not-collected/input-bounded', 'предел inference', 'header, handshake или error'],
['Средства', 'not-run/not-collected', 'список будущих ограничений', 'command/output/config'],
['Вывод', 'synthetic-plan-hand-off', 'следующий decision point', 'success, remediation, production effect'],
const field = revision({
slug: 'editorial-2027-05-field-http-tls-guide',
title: 'Как передавать сетевую ошибку: короткая запись без утечки токенов',
categories: ['Сети', 'Инженерные практики'],
cover: '/assets/editorial/2027/http-tls-guide-2027-evidence-handoff-loop.svg',
excerpt: 'Формат сетевой заметки, который помогает следующему инженеру продолжить проверку и не раскрывает секреты.',
readingMinutes: 14,
}, [
p('Сетевая ошибка часто передаётся одной строкой: «HTTPS не работает». Цена такой записи — повторить тот же эксперимент, потерять hostname за маской или отправить в чат токен из вывода <code>curl -v</code>. Получателю нужны не все байты, а минимальный набор фактов, по которому можно выбрать следующий безопасный запрос.'),
p('Ниже соберём полевую карточку для одного обращения: отделим входные данные, наблюдение и гипотезу, затем применим простую очистку вывода. Учебный код принимает текст, удаляет секретные заголовки и сохраняет строки, необходимые для различения TLS и HTTP. Он не отправляет данные и не пишет файл.'),
h2('Карточка должна разделять факт и гипотезу'),
p('Факт — это то, что клиент действительно увидел: код, текст библиотеки, время соединения, имя узла и безопасная часть ответа. Гипотеза — «вероятно, просрочен сертификат» или «путь переписал proxy». Если смешать их в одном поле, следующий инженер примет предположение за результат и начнёт проверку с неверного уровня.'),
table('Поля сетевой карточки', ['Поле', 'Пример', 'Роль'], [
['Цель', 'api.example.test', 'какое имя проверяли'],
['Метод и путь', 'GET /health', 'какой HTTP-контракт вызван'],
['Этап', 'TLS или HTTP', 'где появился первый факт'],
['Наблюдение', '404, тело missing', 'что вернул клиент'],
['Гипотеза', 'маршрут не смонтирован', 'что проверить дальше'],
['Следующий запрос', 'GET /health', 'ожидаемый результат'],
]),
h2('Provenance сильнее убедительного тона'),
p('У поля evidence должен быть не только label, но и допустимая сила. В сценарии incoming literal — единственный материал. Если evaluator вдруг возвращает certificate conclusion, конкретную protocol version или success claim, он делает output сильнее input. Именно такая подмена закрывается отдельным stop. Формулировка «кажется, это TLS» тоже не нейтральна: она приписывает классу будущей проверки больше, чем дано входом. Честная передача использует «требуется различить» вместо «обнаружено».'),
p('Fail-closed важнее гладкого hand-off. Недатированный кейс не получает текущую дату автоматически: иначе future plan тихо превращается в report. Неназванный protocol/version question не получает conventional default: иначе «HTTP» и «TLS» становятся фоновой догадкой. Скрытая command/endpoint/config не получает placeholder: иначе future owner примет метод как уже заданный. В каждом из этих случаев stop — не ошибка пользователя, а сохранение права пересобрать вопрос до доступа к сети.'),
h2('Стандарты задают vocabulary, не факт'),
p('Pinned RFC 9110, RFC 9112, RFC 9114 и RFC 8446 опубликованы до cutoff и служат первичными источниками vocabulary: semantics HTTP, HTTP/1.1 framing, HTTP/3 and QUIC mapping, TLS 1.3 handshake. Они не дают контекст конкретного клиента. Нельзя взять определение из RFC и выдать его за сведения о current deployment; нельзя узнать из него certificate chain или точный error. Такая разница кажется очевидной в лаборатории, но исчезает в краткой полевой заметке — поэтому здесь она записана явно.'),
p('P111 намеренно не утверждает current cURL/OpenSSL behavior. Для подобного утверждения нужен exact pinned release source и scope, который связывает release с конкретной допустимой конфигурацией; этого нет. Даже тогда documentation не была бы trace. Пока tools не разрешены, future owner получает не рецепт, а вопрос: какой инструмент вообще нужен, какие параметры являются чувствительными, какой output допустимо сохранить и что будет считаться недостаточным evidence.'),
h2('Runnable контракт hand-off'),
code("import { createFixedHttpTlsCase, assessFixedHttpTlsPlan } from './upgrade-2027-05.mjs';\n\nconst hiddenMethod = createFixedHttpTlsCase('hidden-command-endpoint-or-config-v1');\nconst result = assessFixedHttpTlsPlan(hiddenMethod);\nconsole.log({ status: result.status, effect: result.productionEffect });\n// { status: 'stop-hidden-command-endpoint-or-config', effect: 'not-attempted' }"),
p('Пример демонстрирует отказ, а не результат подключения. Все значения fixed и in-memory; JSON clone не даёт caller менять исходный nested object, deep freeze закрепляет protocolQuestion и evidence, fail-closed evaluator отклоняет unknown input. В модуле нет network library, shell invocation, HTTP parser, TLS implementation, file read или process configuration. Поэтому runnable behaviour можно проверить локально без того, чтобы создать trace или попытаться воздействовать на production.'),
h2('Порядок передачи'),
ol(['Передать дату <code>2027-05</code> и cutoff <code>2026-07-31</code> как обязательную границу знания.', 'Передать только named protocol/version question и statement, не добавляя endpoint, hostname, certificate или client name.', 'Передать evidence как <code>not-collected/input-bounded</code>; не превращать отсутствие данных в нейтральный success.', 'Явно оставить cURL/OpenSSL, commands, traces и configuration в состоянии not-run/not-collected.', 'Проверить fail-closed stops для undated case, unnamed question, stronger evidence, hidden method и positive conclusion.', 'Отдать future evidence owner next action без queue mutation, ticket, test report, remediation или production change.' ]),
h2('Минимальная карточка лучше ложной полноты'),
p('Полевой hand-off часто пытаются сделать полезным добавлением «кажется очевидных» деталей. Появляется пример host, предполагаемый port, название trust store, фрагмент команды или краткое описание certificate. Для будущего расследования это кажется экономией времени, но provenance у таких деталей нулевой: читатель не знает, увидел ли автор их, получил из документа или просто дополнил по памяти. В P111 минимальная карточка сознательно не содержит ни одного такого ускорителя.'),
p('Минимум не равен расплывчатости. Карточка обязана ясно назвать, что нужно различить, когда knowledge зафиксировано и какие состояния недопустимы. Поэтому fixed literal строже свободной заметки: у него есть exact id, date/cutoff, nested question, evidence state и disallowed conclusion. Неопределённость здесь структурирована. Owner не получит загадку «что-то с HTTPS»; он получит проверяемое ограничение, что пока нельзя рассказывать о peer, сообщении или инструменте.'),
p('Такой формат облегчает и отказ. Получатель может вернуть hand-off с формулировкой «вопрос не имеет достаточного решения о доступе» без дискуссии о якобы собранном trace. Он может попросить автора уточнить цель на уровне приложения, а не сети. Он может передать вопрос security owner, не распространяя endpoint. Все эти действия оставляют исходный artefact истинным. Ложный отчёт, напротив, заставляет либо верить ему, либо повторять неизвестную методику, что дороже и опаснее.'),
p('Карточку нельзя улучшать добавлением «вероятного» результата. Даже осторожное «ожидается нормальный handshake» создаёт асимметрию: следующий reader помнит прогноз, но не помнит, что evidence отсутствует. Аналогично phrase «должен вернуться header» звучит как план, но фактически обещает content неизвестного сообщения. В P111 будущему owner передаётся не ожидание output, а список условий, которые придётся определить до первого authorized observation. Это делает последующую проверку свободной отменить исходную гипотезу.'),
p('Синтетический hand-off полезно хранить рядом с границами, а не рядом с логами. Его читатель не ищет в нём bytes или timestamp; он ищет, какую claim нельзя сделать и кому передать решение. Когда реальный scope появится, его artefact должен ссылаться на P111 лишь как на предшествующую постановку, а не как на evidence. Такая связь оставляет историческую цепочку понятной: сначала была редакционная дисциплина, затем — отдельно разрешённый сбор, если он вообще окажется необходим.'),
p('Именно поэтому результат hand-off не измеряется количеством технических деталей. Его достаточно, когда другой человек может безопасно остановиться, сузить вопрос или открыть новый scope, не переписывая историю как будто проверка уже состоялась.'),
p('Если этого предела нет, заметка перестаёт быть передачей и становится неаудируемым обещанием: её нельзя ни проверить, ни корректно отозвать без потери доверия. Поэтому правильная краткость здесь ценнее демонстративной технической полноты.'),
h2('Что owner решает вне P111'),
p('Если будущий владелец примет hand-off, ему понадобятся отдельные решения: нужен ли вообще сетевой scope, кто авторизует target, какая минимальная configuration допустима, какие поля trace нельзя сохранять, как redaction влияет на inference и кто проверяет вывод. Может оказаться, что вопрос надо закрыть без запуска, потому что нужный факт есть в другом авторитетном artefact или риск сбора не оправдан. P111 не подталкивает к инструменту и не создаёт обязанность «довести проверку до успеха».'),
p('Следующий шаг — завести новый, явно авторизованный artefact только если owner выберет это действие. Он должен иметь собственную дату, source set, method, limits и result policy; его facts нельзя ретроспективно добавить в P111. До такого шага текущий итог уже полный: передан честно ограниченный вопрос. Полезность field hand-off не в том, что он производит network result, а в том, что он не даёт отсутствующему результату управлять последующими решениями.'),
figure('/assets/editorial/2027/http-tls-guide-2027-evidence-handoff-loop.svg', 'Цикл сетевой диагностики: безопасный сбор, классификация этапа, проверка гипотезы и повторяемая запись результата.', 'Схема показывает круг работы с одной ошибкой. На каждом переходе остаётся только нужный факт, а секретные поля отбрасываются до передачи.'),
h2('Почему вывод curl требует очистки'),
p('Подробный вывод полезен для порядка рукопожатия, редиректов и заголовков, но в нём могут оказаться <code>Authorization</code>, cookie, query-параметры и внутренние адреса. Маскирование должно работать до копирования в задачу или чат. Не полагайтесь на память: человек легко пропустит строку, если вывод длинный.'),
p('Отдельно проверьте заголовки proxy-аутентификации и значения после редиректа: очистка только строки <code>Authorization</code> не покрывает все каналы секрета. В карточке оставьте имя поля и замените значение целиком, чтобы получатель понимал, какой тип аутентификации был задействован.'),
p('Очистка не должна удалять метод, статус и имя заголовка. Иначе получатель увидит «что-то с HTTP», но не сможет различить 401 и 403. Значение <code>Authorization</code> заменяем целиком, cookie очищаем по имени, а query оставляем только после удаления секретных ключей. Для персональных данных нужен отдельный список полей.'),
h2('Учебный санитайзер текстового вывода'),
p('Входом функции является обычная многострочная строка. Ожидаемый результат — тот же порядок строк, но значение Authorization и Cookie заменены. Пример запускается без сети; он показывает обработку диагностического artefact, а не проверяет сертификат.'),
code([
'function redactNetworkOutput(text) {',
' return text',
" .replace(/(Authorization:\\s*Bearer\\s+)[^\\s]+/gi, '$1[masked]')",
" .replace(/(Cookie:\\s*)[^\\n]+/gi, '$1[masked]')",
" .replace(/([?&](?:token|secret|signature)=)[^&\\s]+/gi, '$1[masked]');",
'}',
'',
"const raw = 'GET /health?token=abc HTTP/1.1\\nAuthorization: Bearer abc\\nCookie: sid=xyz';",
'console.log(redactNetworkOutput(raw));',
'// GET /health?token=[masked] HTTP/1.1',
'// Authorization: Bearer [masked]',
'// Cookie: [masked]',
].join('\n')),
p('Проверка результата здесь буквальная: в трёх строках не осталось исходных значений, а имена полей сохранились. Регулярное выражение учебное и намеренно ограниченное. Оно не понимает бинарные данные, нестандартное форматирование и секреты в произвольном JSON, поэтому перед передачей нужен отдельный просмотр очищенного вывода.'),
h2('Сохраняем причинную цепочку'),
p('Хорошая карточка отвечает на четыре вопроса. Что вызвали? Где остановился клиент? Что именно получено? Какой следующий запрос отличит две гипотезы? Например: «GET /health, TLS завершён, HTTP 503 и Retry-After: 2, следующий шаг — повторить GET через две секунды и сравнить зависимость». Это уже проверяемый маршрут, а не комментарий «сервер тормозит».'),
p('Не нужно прикладывать полный дамп, если достаточно нескольких строк. При этом нельзя вырезать контекст, который меняет смысл: hostname, порт, метод и статус должны остаться. Время указывайте вместе с часовым поясом, а длительность — с единицей измерения. Если был proxy, напишите это явно, иначе получатель будет считать соединение прямым.'),
h2('Когда 401, 403 и 404 похожи'),
table('Статус и следующий вопрос', ['Статус', 'Наблюдаемая семантика', 'Проверка'], [
['401', 'нужна аутентификация или она не принята', 'какой challenge вернул сервер'],
['403', 'сервер понял запрос, но отказывает', 'правило доступа и origin запроса'],
['404', 'ресурс не найден по выбранному маршруту', 'путь, метод и версия API'],
['405', 'метод не разрешён для ресурса', 'Allow и контракт метода'],
]),
p('Текст страницы может быть одинаковым у разных кодов, особенно на proxy. Поэтому в карточке статус важнее заголовка «access denied». RFC 9110 описывает классы статусов и методы, но не знает правила конкретного приложения. Источник задаёт язык сообщения; фактическая причина появляется только из вашего наблюдения и сопоставленного лога.'),
h2('Порядок безопасной передачи'),
ol([
'Скопировать исходный вывод во временную локальную область, не отправляя его в общий канал.',
'Удалить Authorization, Cookie, токены в query и внутренние персональные значения.',
'Оставить hostname, порт, метод, путь без секретных параметров, этап, статус и длительность.',
'Отделить наблюдение от гипотезы и не называть гипотезу причиной.',
'Сформулировать один следующий запрос и его ожидаемый ответ.',
'После повторной проверки заменить гипотезу новым фактом или явно сохранить её как неподтверждённую.',
]),
h2('Ограничения и следующий шаг'),
p('Санитайзер не является системой управления секретами и не гарантирует, что неизвестный формат не содержит чувствительных данных. Не вставляйте очищенный текст в публичный issue без проверки. Для постоянной диагностики лучше использовать структурированные поля с allowlist, чем регулярно маскировать свободный текст.'),
p('Следующим шагом заведите шаблон карточки в репозитории: цель, метод, этап, статус, длительность, гипотеза и ожидаемый результат. Добавьте к нему локальный тест на маскирование трёх известных секретов и один отрицательный пример. Тогда следующая ошибка будет начинаться с проверяемого входа, а не с повторного поиска контекста.'),
], refs);
export const revisions = deepFreeze([practice, mechanism, field]);
export function verifyRevisionsAgainstFixture() { const fixture = runFixedHttpTlsFixture(); const articleChecks = revisions.map((item) => { const text = bodyText(item.contentHtml); return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|издержк|потер)/i.test(text.slice(0, 1100)) && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && /2027-05/.test(text) && /2026-07-31/.test(text) && /productionEffect: not-attempted/.test(text); }); return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) }); }
if (process.argv.includes('--verify-fixture')) { const result = verifyRevisionsAgainstFixture(); process.stdout.write(JSON.stringify(result, null, 2) + '\n'); if (!result.accepted) process.exitCode = 1; }
export function verifyRevisionsAgainstFixture() {
const fixture = runFixedHttpTlsFixture();
const articleChecks = revisions.map((item) => {
const text = bodyText(item.contentHtml);
return text.length >= 5000 && text.length <= 15000 && /(цен[аы]|стоимост|затрат|потер)/i.test(text.slice(0, 1100)) && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml);
});
return deepFreeze({ passed: fixture.passed + articleChecks.filter(Boolean).length, total: fixture.total + articleChecks.length, accepted: fixture.accepted && articleChecks.every(Boolean), fixture, articleChecks, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) });
}
if (process.argv.includes('--verify-fixture')) {
const result = verifyRevisionsAgainstFixture();
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
if (!result.accepted) process.exitCode = 1;
}
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');