Веб-разработка
HTTP в браузере: fetch, CORS и кэш
1999 год: Microsoft изобретает XMLHttpRequest для Outlook Web Access. 2004 год: Gmail использует его для фоновых запросов без перезагрузки страницы - и меняет представление о том, чем может быть веб-приложение. 2015 год: Fetch API заменяет 150 строк XMLHttpRequest-кода одной строкой. Но одновременно добавляет CORS, preflight-запросы и незакономерное поведение при ошибках - которые ломают приложения в production и по сей день.
- fetch() к OpenAI/Anthropic API из браузера - POST с Bearer-токеном, обработка 429 rate-limit и streaming через ReadableStream
- CORS-ошибки при разработке: frontend на localhost:3000 обращается к backend на localhost:8080 - типичная ситуация при работе с ML-инференс сервисами
- Cache-Control: immutable для JS/CSS бандлов с хэшем в имени - стандарт Webpack/Vite для нулевого времени загрузки повторных визитов
- Service Worker кэш для ONNX-моделей в браузере - офлайн-инференс без повторной загрузки сотен мегабайт
HTTP-методы в fetch()
2004 год. Google запускает Gmail. Все остальные веб-приложения работают по одной схеме: нажал кнопку - страница перезагрузилась. Gmail - нет. Microsoft изобрёл XMLHttpRequest ещё в 1999 году для Outlook Web Access, но именно Gmail показал, что браузер способен отправлять HTTP-запросы в фоне и обновлять только часть страницы. Джесси Джеймс Гаррет назвал это Ajax в 2005 году. С тех пор браузерные HTTP-запросы - это основа любого интерактивного интерфейса.
В 2015 году появился **Fetch API** - Promise-based замена XMLHttpRequest. GET-запрос теперь занимает одну строку вместо 15. Но за простотой скрывается архитектурное решение: `fetch()` принимает метод явно, и выбор метода влияет на поведение браузера и сервера.
**Preflight-запрос:** браузер автоматически отправляет OPTIONS-запрос перед PUT, DELETE, PATCH и перед любым POST с `Content-Type: application/json`. Это механизм CORS - браузер спрашивает сервер: «можно мне отправить такой запрос?». Простые GET и POST с `application/x-www-form-urlencoded` preflight не вызывают.
**Method override:** старые прокси и некоторые серверы не понимают PUT/DELETE. Паттерн обхода - POST с заголовком `X-HTTP-Method-Override: PUT`. Браузерный `fetch()` поддерживает все методы напрямую, но при отладке через curl или Postman это важно знать.
Браузер автоматически отправляет OPTIONS preflight перед:
Заголовки запросов и CORS
Парадокс CORS: он **не защищает сервер**. Сервер получает и обрабатывает все запросы - и легитимные, и злоумышленные. CORS защищает **браузер пользователя от его же JavaScript**. Если сайт `evil.com` пытается читать данные из `bank.com` через fetch(), браузер проверяет: разрешил ли `bank.com` запросы с `evil.com`. Без этой проверки любой скрипт на любой странице мог бы читать куки и данные любого другого домена.
**Кастомные заголовки вызывают preflight.** Любой заголовок кроме `Accept`, `Accept-Language`, `Content-Language` и `Content-Type` (только с простыми значениями) - нестандартный. Это включает `Authorization`, `X-Request-ID`, `X-Api-Key` и любые кастомные `X-*`. Добавление кастомного заголовка к GET-запросу превращает его в «непростой» и добавляет roundtrip на preflight.
**Access-Control-Max-Age** кэширует preflight-ответ в браузере. Без этого заголовка браузер будет делать OPTIONS перед каждым fetch(). Для ML-инференса, где каждый запрос - дорогой POST с Authorization, preflight-кэш снижает latency и нагрузку на сервер.
GET-запрос с заголовком Authorization к другому домену - это:
Коды статусов и обработка ошибок в fetch()
200 OK при `fetch()` не означает успех. `Response.ok` может быть `false`. Это не баг - это архитектура Fetch API. В отличие от XMLHttpRequest, `fetch()` не выбрасывает исключение при HTTP-ошибках (4xx, 5xx). Promise резолвится успешно если сервер ответил вообще. Reject происходит только при сетевых ошибках: нет соединения, DNS не резолвится, CORS заблокировал ответ. Разработчик обязан проверять `response.ok` или `response.status` самостоятельно.
**Категории статусов:** 1xx - информационные (101 Switching Protocols для WebSocket). 2xx - успех (200 OK, 201 Created, 204 No Content). 3xx - редиректы (301 Moved Permanently, 304 Not Modified для кэша). 4xx - ошибки клиента (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests). 5xx - ошибки сервера (500 Internal Server Error, 503 Service Unavailable).
fetch('/api/data') - сервер вернул 500. Promise будет:
Кэш браузера: Cache-Control, ETag и Service Worker
Браузерный кэш - не просто «копия файла». Это многоуровневая система с несколькими стратегиями хранения. HTTP/1.0 использовал `Expires` с абсолютной датой. HTTP/1.1 заменил его `Cache-Control` с относительными директивами. Service Worker API (2015) добавил третий уровень - программируемый кэш под полным контролем JavaScript. Каждый слой отвечает за разные сценарии: от статических ассетов до offline-режима.
**ETag vs Last-Modified:** ETag - хэш содержимого, точнее. Last-Modified - временная метка, может быть неточной (файл изменился и вернулся к исходному). При наличии обоих браузер предпочтёт ETag. CDN Cloudflare/Fastly генерируют ETag автоматически для статических файлов.
**Cache-Control: no-cache vs no-store:** `no-cache` кэширует локально, но проверяет актуальность при каждом запросе - если сервер возвращает 304, браузер использует кэш без загрузки. `no-store` - не кэшировать вообще, каждый раз полная загрузка. Для чувствительных данных (банковские операции) нужен `no-store`. Для HTML-страниц достаточно `no-cache`.
fetch() выбрасывает ошибку при 404 или 500, поэтому достаточно try/catch
fetch() reject происходит только при сетевых ошибках (нет соединения, CORS-блокировка). HTTP-ошибки 4xx и 5xx резолвятся успешно - их нужно проверять через response.ok или response.status
Это архитектурное решение Fetch API: HTTP-ответ с кодом 500 - это всё равно успешно полученный ответ от сервера. Ошибкой считается ситуация когда ответ не получен вообще. try/catch поймает только сетевые сбои, а бизнес-логика должна обрабатывать HTTP-статусы явно
Сервер вернул Cache-Control: no-cache. При следующем запросе браузер:
HTTP в браузере: ключевые идеи
- fetch() не выбрасывает исключение при 4xx/5xx - проверяй response.ok явно
- CORS защищает браузер от межсайтового чтения данных, а не сервер от запросов
- Кастомные заголовки (Authorization) и нестандартные Content-Type вызывают preflight OPTIONS
- Cache-Control: no-cache не отключает кэш - он требует валидации через 304 Not Modified
Связанные темы
HTTP в браузере - прикладной слой над сетевым стеком. Понимание нижних слоёв объясняет ограничения и поведение fetch().
- DOM и браузерные API — fetch() - браузерный API, работает в том же event loop что и DOM-манипуляции
- HTTP фундаментals — Детали протокола: TCP-соединения, HTTP/2 мультиплексирование, которые браузер абстрагирует в fetch()
- Безопасность веб-приложений — CSP, SameSite куки и другие механизмы безопасности браузера, дополняющие CORS
- Интеграция с AI API — Паттерны работы с LLM API через fetch(): streaming, retry, rate limiting
Вопросы для размышления
- Почему CORS-политика не мешает curl или Postman делать те же запросы что блокирует браузер?
- В каком сценарии `Cache-Control: no-store` предпочтительнее `no-cache`, и как это влияет на производительность?
- Что произойдёт если Service Worker закэширует ответ LLM API и вернёт его при следующем запросе с другим промптом?