Как проверить API: DNS, TLS, заголовки и CORS

Ключевой вывод

«API не работает» почти никогда не означает одну проблему. Проверяйте по шести слоям (DNS, порт 443, TLS, статус HTTP, CORS, тело ответа) и останавливайтесь на первом, который не прошёл. Типичные виновники: DNS-запись, которая не распространилась, сертификат, истёкший за ночь, или отсутствующий заголовок Access-Control-Allow-Origin, из-за которого ломаются вызовы из браузера, хотя curl работает.

Проверьте сейчас

Схема шести слоёв проверки публичного API: разрешение DNS, порт 443, сертификат TLS, заголовки HTTP, CORS и тело JSON
Схема шести слоёв проверки публичного API: разрешение DNS, порт 443, сертификат TLS, заголовки HTTP, CORS и тело JSON
Начать чтение
  1. Почему «API не работает» никогда не бывает одной проблемой
  2. Выберите настоящий эндпоинт, который не жалко нагружать
  3. Слой 1: разрешается ли имя хоста?
  4. Слой 2: открыт ли порт 443?
  5. Слой 3: действителен ли сертификат TLS?
  6. Слой 4: читаем заголовки ответа
  7. Как выглядит здоровый JSON-ответ
  8. Слой 5: заголовок, который ломает только вызовы из браузера
  9. Ловушка preflight: работает, пока не добавите заголовок
  10. Слой 6: соответствует ли тело ответа ожидаемой форме?
  11. Симптом → слой → инструмент
  12. Часто задаваемые вопросы

Почему «API не работает» никогда не бывает одной проблемой

Когда вызов API не удаётся, ошибку, которую показывает клиент, формирует слой на несколько уровней выше того, что действительно сломалось. Браузер с сообщением Failed to fetch не скажет, не разрешилось ли имя хоста, отказано ли в TCP-соединении, отклонено ли TLS-рукопожатие или сервер вернул вполне здоровый 200, который браузер отбросил из-за отсутствия заголовка CORS. Во всех четырёх случаях сообщение одно и то же.

Хватит гадать: пройдите стек по порядку. Каждый слой имеет смысл только если сработал нижний, поэтому первый упавший слой и есть настоящая ошибка, а всё после него шум.

Стоит проверить шесть слоёв, и вместе это занимает около двух минут.

  • DNS: разрешается ли имя хоста в IP вообще?
  • Порт 443: слушает ли что-нибудь, или соединение отклонено либо отфильтровано?
  • TLS: действителен ли сертификат, не истёк ли он, полная ли цепочка?
  • Статус HTTP: что сервер реально возвращает: 200, 401, 429, 503?
  • CORS: есть ли в ответе заголовки, нужные браузеру, чтобы отдать вам тело?
  • Тело ответа: соответствует ли JSON форме, которую ждёт ваш код?

Проверьте статус и заголовки эндпоинта сейчас

Выберите настоящий эндпоинт, который не жалко нагружать

Чтобы потренироваться, нужен живой эндпоинт, который возвращает реальные данные, не требует ключа API и не включит лимит, если выполнить один запрос двадцать раз. Это отсекает большинство очевидных кандидатов: почти любой популярный публичный API теперь требует регистрации и токена, так что неудачный запрос может оказаться проблемой учётных данных, а не проверяемого слоя.

В примерах ниже используется бесплатный API времени намаза от Waqt Azan (مواقيت الصلاة), арабского сервиса времени молитв, охватывающего более 60 000 мест в более чем 100 странах. Он хорошо подходит как учебный эндпоинт по трём причинам: нет ключа и заголовка авторизации, он возвращает структурированный JSON, а не HTML, и у него мягкая политика CORS. Поэтому команды curl выполняются как написано, а пример с fetch() работает из консоли страницы без строгой Content-Security-Policy.

Эндпоинт принимает страну и slug города прямо в пути:

bash
# Prayer times for a city — no API key, no headers required
curl -s https://waqtazan.com/api/v1/timings/saudi-arabia/riyadh

# The same data as a subscribable calendar feed
curl -s https://waqtazan.com/api/v1/ical/saudi-arabia/riyadh.ics

Подставьте любой хост, который вы реально отлаживаете: шесть слоёв одинаковы, тестируете ли вы публичный API, собственный staging-сервер или приёмник сторонних вебхуков.

Слой 1: разрешается ли имя хоста?

DNS частый виновник. Запись изменили, и она не распространилась, wildcard удалили или поддомен для API вообще не создали.

Сначала разрешите имя хоста. Если здесь ничего не вернулось, сколько ни перезапускай приложение, не поможет: клиент даже не дойдёт до открытия сокета.

bash
dig +short waqtazan.com A
nslookup waqtazan.com

# Compare what different resolvers see — catches mid-propagation records
dig +short waqtazan.com A @1.1.1.1
dig +short waqtazan.com A @8.8.8.8

Если два публичных резолвера расходятся сразу после изменения записи, она ещё распространяется, и API будет казаться то работающим, то нет, в зависимости от того, к какому резолверу попал клиент (CDN с гео-маршрутизацией может постоянно отдавать разным резолверам разные адреса, поэтому сравнивайте с записью, которую вы задали). Запустите DNS-запрос, чтобы увидеть полный набор записей, или проверьте распространение на глобальных резолверах в проверке DNS-распространения сразу после изменения записи.

Пустой ответ здесь напрямую соответствует ERR_NAME_NOT_RESOLVED в Chrome и getaddrinfo ENOTFOUND в Node. Полный список решений есть в статье ERR_NAME_NOT_RESOLVED.

Об одном беспокоиться не нужно: многие API стоят за CDN, поэтому A-запись указывает на узел CDN, а не на исходный сервер. Это нормально. Важно, что ответ вообще есть, а не какой именно IP.

Слой 2: открыт ли порт 443?

Имя хоста разрешилось, значит, адрес есть. Теперь выясните, слушает ли на нём что-нибудь. Здесь разделяются три совершенно разных сбоя, которые клиенты любят сообщать одинаково:

  • Открыт (Open): TCP-рукопожатие завершается. Переходите к следующему слою.
  • Отказ (Refused): хост сразу ответил RST. Хост на месте, но к порту ничего не привязано: обычно сервис упал или слушает только localhost.
  • Отфильтрован (Filtered): соединение висит до тайм-аута. Файрвол или группа безопасности молча отбрасывает пакеты, поэтому запрос висит минуту или дольше, прежде чем завершиться ошибкой (75 секунд в macOS).
bash
# Time-boxed TCP check: -w limits the connect on Linux; on macOS use -G 5 instead
nc -vz -w 5 waqtazan.com 443

# Or with curl: --connect-timeout caps the connect; 000 means no response came back
curl -s -o /dev/null -w '%{http_code}\n' --connect-timeout 5 https://waqtazan.com/

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

Если curl печатает 000 вместо кода статуса, HTTP-ответ не пришёл. Обычно сбой произошёл в слоях 1–3 (нет ответа DNS, порт отклонён или отфильтрован, не удалось TLS-рукопожатие), но сервер, который принимает соединение и затем закрывает его или не отвечает, тоже даёт 000. Добавьте -v, чтобы увидеть последний сработавший шаг. Случай отказа подробно разобран в ERR_CONNECTION_REFUSED.

Слой 3: действителен ли сертификат TLS?

Порт 443 открыт, но открытый порт не означает рабочий HTTPS-эндпоинт. Сертификаты классически ломаются за ночь: они истекают по расписанию, а автоматическое продление тихо перестаёт работать.

Должны выполняться сразу три условия: сертификат не истёк, его имя совпадает с запрошенным хостом, а сервер отправляет полную цепочку промежуточных сертификатов. Третье коварнее всего: неполная цепочка часто работает в браузерах, потому что Chrome сам докачивает недостающий промежуточный сертификат, а Firefox заранее их загружает, тогда как Node, Python, Java и сборки curl на OpenSSL выдают ошибку (curl из macOS использует проверку Apple и может пройти). Отсюда классический баг-репорт «в браузере работает, а в продакшене нет».

bash
# Expiry dates and subject
echo | openssl s_client -connect waqtazan.com:443 -servername waqtazan.com 2>/dev/null \
  | openssl x509 -noout -subject -dates

# Full chain as presented by the server
openssl s_client -connect waqtazan.com:443 -servername waqtazan.com -showcerts < /dev/null

Посчитайте сертификаты в выводе -showcerts. Один-единственный сертификат обычно означает пропущенный промежуточный. Проверка SSL-сертификата за один проход сообщает о сроке действия, полноте цепочки и совпадении имени хоста, а что такое цепочка SSL-сертификатов объясняет, почему промежуточный сертификат так важен.

Оставляйте -servername: OpenSSL 1.1.1 и новее подставляют SNI из имени в -connect, но LibreSSL openssl из macOS этого не делает, а при подключении по IP-адресу не делает ни одна версия. Без SNI виртуальный хост отдаёт сертификат по умолчанию или, как Cloudflare, обрывает рукопожатие, и вы гоняетесь за несоответствием имени, которого на самом деле нет.

Слой 4: читаем заголовки ответа

С TLS всё в порядке, и наконец появляется настоящий HTTP-диалог. Теперь прочитайте, что сервер сказал на самом деле, а не то, что вам пересказал клиент. Выводим только заголовки: тело всё равно скачивается и отбрасывается, так что это тот же GET, что отправляет ваш клиент.

bash
curl -s -D - -o /dev/null https://waqtazan.com/api/v1/timings/egypt/cairo

Сначала читайте строку статуса, затем content-type, потом всё остальное. Эти два поля говорят, успешен ли запрос и является ли тело JSON, прежде чем вы дочитаете остальное.

Как выглядит здоровый JSON-ответ

Выполнение команды выше против тестового эндпоинта 27 октября 2026 года вернуло среди прочих такие заголовки:

ЗаголовокЗначениеЧто это говорит
HTTP/2200Запрос выполнен, сервер работает по HTTP/2
content-typeapplication/jsonВы получите JSON, а не HTML-страницу с ошибкой
content-length567Это реальное тело, а не пустой ответ
access-control-allow-origin*Браузерам разрешено читать этот ответ
servercloudflareПеред исходным сервером стоит CDN
alt-svch3=":443"Для следующих запросов объявлен HTTP/3

Слой 5: заголовок, который ломает только вызовы из браузера

На этом слое пропадают часы, потому что именно здесь curl и браузер действительно расходятся. CORS проверяет браузер, а не сервер. Если Access-Control-Allow-Origin отсутствует, сервер всё равно возвращает полный и правильный 200, а браузер просто отказывается отдать тело вашему JavaScript.

Поэтому симптом такой: curl работает отлично, Postman работает отлично, а в консоли браузера написано, что запрос заблокирован политикой CORS. С сервером всё в порядке. В ответе просто не хватает одного заголовка.

Тестовый эндпоинт возвращает access-control-allow-origin: *, поэтому обычный fetch() работает с любого origin:

javascript
// Run it in the console of a tab on a site without a strict CSP, such as example.com
// (this page's own CSP blocks it). It works there because ACAO is set to *
const r = await fetch('https://waqtazan.com/api/v1/timings/egypt/cairo')
console.log(await r.json())

Ловушка preflight: работает, пока не добавите заголовок

Есть и вторая, менее очевидная ловушка, и этот эндпоинт хорошо её показывает. Простой запрос (обычный GET без пользовательских заголовков) проходит. Но как только вы добавляете пользовательский заголовок или делаете POST с Content-Type: application/json, браузер сначала отправляет preflight-запрос OPTIONS, и ответ на него должен нести собственные заголовки CORS (MDN).

Проверяйте preflight отдельно, потому что это другой ответ из другой ветки кода:

bash
curl -s -D - -o /dev/null -X OPTIONS \
  -H 'Origin: https://example.com' \
  -H 'Access-Control-Request-Method: GET' \
  https://waqtazan.com/api/v1/timings/egypt/cairo

На 27 октября 2026 года этот preflight возвращает 200 с content-type: text/html и вообще без заголовка access-control-allow-origin, поэтому запрос из браузера с preflight был бы заблокирован, хотя обычный GET работает.

Это и есть ловушка: интеграция работает в разработке, вы добавляете заголовок Authorization или X-Request-Id, и она ломается без каких-либо изменений на сервере. Если вам нужны только публичные данные, делайте запрос простым, и он продолжит работать.

Слой 6: соответствует ли тело ответа ожидаемой форме?

Последний слой это то, что действительно важно вашему коду. Ответ 200 с корректным JSON всё равно может сломать вас, если поле переименовали, тип сменился с числа на строку или метка времени пришла в формате, который вы не разбираете.

Посмотрите на реальный ответ, прежде чем писать парсер. Этот эндпоинт возвращает и время в формате для показа, и ISO-метки времени, и этому шаблону стоит подражать: локализованные строки для людей, ISO-значения для кода:

json
{
  "city": { "slug": "riyadh", "name_ar": "الرياض", "name_en": "Riyadh" },
  "country": "saudi-arabia",
  "date": "2026-10-26",
  "times":    { "fajr": "4:38 ص", "dhuhr": "11:37 ص", "maghrib": "5:17 م" },
  "timesISO": { "fajr": "2026-10-26T01:38:00.000Z", "dhuhr": "2026-10-26T08:37:00.000Z" },
  "method": "umm_al_qura"
}

Всегда разбирайте машиночитаемое поле. Здесь это timesISO, а не times: строки для показа локализованы и сменят формат, как только сменится локаль.

Чтобы быстро изучить структуру, пропустите ответ через jq: curl -s <url> | jq keys выводит поля верхнего уровня, а jq '.times' позволяет заглянуть внутрь без прокрутки сырого тела.

Симптом → слой → инструмент

Используйте таблицу, чтобы сразу перейти к нужному слою, а не проходить все шесть каждый раз:

Что вы видитеКакой слой сломанЧем проверить
ERR_NAME_NOT_RESOLVED, ENOTFOUND1 — DNSDNS-запрос
ECONNREFUSED, instant failure2 — ПортПроверка портов
Запрос зависает, затем тайм-аут2 — Отфильтрованный портПроверка портов
CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE3 — TLSПроверка SSL
В браузере работает, в curl или Node нет3 — Неполная цепочкаПроверка SSL
401, 429, 5034 — Статус HTTPПроверка HTTP-заголовков
В curl работает, в браузере заблокировано5 — CORSПроверка HTTP-заголовков
Unexpected token '<', "<!DOCTYPE "... is not valid JSON4 — HTML-страница с ошибкойПроверка HTTP-заголовков
Нет полей или неверный тип6 — Тело ответаcurl … | jq

Задержка немного выходит за рамки этой лестницы. Если все слои проходят, но ответы медленные, замерьте время кругового пути до хоста с помощью ping и проверьте, не узкое ли место само разрешение имён, тестом скорости DNS.

Проверьте реальные заголовки ответа любого эндпоинта

Посмотрите точный код статуса, content-type и заголовки CORS, которые возвращает API. Запрос идёт с серверной стороны, поэтому кэш браузера и расширения не искажают результат. Бесплатно, без регистрации.

Источники

  1. Cross-Origin Resource Sharing (CORS), MDN
  2. Fetch Standard: CORS-safelisted request headers, WHATWG

Часто задаваемые вопросы

Как протестировать API-эндпоинт без Postman?

Для базовой проверки curl делает всё то же, что Postman. curl -s -D - -o /dev/null <url> покажет только заголовки ответа, curl -s <url> | jq позволит изучить JSON-тело, а curl -s -o /dev/null -w '%{http_code}' напечатает только код статуса. Для быстрой проверки совсем без терминала онлайн-проверка HTTP-заголовков выдаёт ту же информацию из браузера.

Почему API работает в curl, но не работает в браузере?

Почти всегда это CORS. Сервер в любом случае возвращает корректный ответ, но браузер не отдаёт тело JavaScript, если в ответе нет заголовка Access-Control-Allow-Origin, разрешающего ваш origin. curl полностью игнорирует CORS, потому что это политика безопасности браузера, а не сервера. Если заголовка нет, его должен отправлять сервер: исправить это на стороне клиента нельзя.

Что означает код статуса 000 в curl?

Это значит, что HTTP-ответ так и не был получен, а значит, сообщать нечего. Обычно сбой произошёл до HTTP: DNS не разрешился, TCP-соединение отклонено или отфильтровано либо не удалось TLS-рукопожатие. Сервер, который принимает соединение и затем закрывает его или не отвечает, тоже даёт 000, поэтому запустите команду снова с -v, чтобы увидеть последний сработавший шаг.

Как понять, что API не работает у всех, а не только у меня?

Проверяйте извне своей сети. Проверка портов и DNS-запрос, выполняемые на стороне сервера, исключают локальный резолвер, VPN, корпоративный файрвол и кэш браузера. Если внешняя проверка эндпоинт видит, а ваша машина нет, проблема локальная: как правило, кэш DNS или правило файрвола.

Почему API возвращает 200, но JSON-парсер выдаёт ошибку?

Скорее всего, вы получаете HTML со статусом 200. Captive portal, прокси и некоторые страницы ошибок возвращают код успеха с HTML-телом. Проверьте заголовок content-type: если там text/html, а не application/json, тело это веб-страница, а ошибка парсинга лишь симптом, а не причина.

Что такое CORS preflight и когда он происходит?

Preflight это автоматический запрос OPTIONS, который браузер отправляет перед основным, если запрос не «простой»: используется метод, отличный от GET, HEAD или POST, заданы пользовательские заголовки вроде Authorization или отправляется Content-Type, отличный от данных формы и обычного текста (application/json в этот список входит). Ответ на preflight должен сам содержать заголовки CORS. Поэтому интеграция может спокойно работать, пока вы не добавите заголовок авторизации, а затем ломается без каких-либо изменений на сервере.

Проверять DNS API через свой резолвер или публичный?

Через оба и сравнивайте. Резолвер провайдера может отдавать устаревшую закэшированную запись ещё долго после изменения авторитетной. Запросы к 1.1.1.1 и 8.8.8.8 наряду с резолвером по умолчанию показывают, задержка ли это распространения или записи действительно нет.

Означает ли открытый порт 443, что HTTPS-эндпоинт работает?

Нет. Открытый порт лишь подтверждает, что что-то приняло TCP-соединение. TLS-рукопожатие всё равно может не удаться: сертификат истёк, имя хоста не совпадает или цепочка промежуточных сертификатов неполная. Проверяйте сертификат отдельно, убедившись, что порт открыт.

Об авторе

Shaik Vahid

Основатель DNS Robot. Вахид много лет работал в поддержке веб-хостинга: исправлял клиентам домены, DNS-записи, SSL-сертификаты и доставку почты. Он создаёт инструменты DNS Robot и пишет эти руководства по ошибкам, с которыми люди сталкиваются на самом деле.

Проверка HTTP-заголовков

Анализ HTTP заголовков ответа и оценка заголовков безопасности