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

Начать чтение
- Почему «API не работает» никогда не бывает одной проблемой
- Выберите настоящий эндпоинт, который не жалко нагружать
- Слой 1: разрешается ли имя хоста?
- Слой 2: открыт ли порт 443?
- Слой 3: действителен ли сертификат TLS?
- Слой 4: читаем заголовки ответа
- Как выглядит здоровый JSON-ответ
- Слой 5: заголовок, который ломает только вызовы из браузера
- Ловушка preflight: работает, пока не добавите заголовок
- Слой 6: соответствует ли тело ответа ожидаемой форме?
- Симптом → слой → инструмент
- Часто задаваемые вопросы
Почему «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 города прямо в пути:
# 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 вообще не создали.
Сначала разрешите имя хоста. Если здесь ничего не вернулось, сколько ни перезапускай приложение, не поможет: клиент даже не дойдёт до открытия сокета.
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).
# 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 и может пройти). Отсюда классический баг-репорт «в браузере работает, а в продакшене нет».
# 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, что отправляет ваш клиент.
curl -s -D - -o /dev/null https://waqtazan.com/api/v1/timings/egypt/cairoСначала читайте строку статуса, затем content-type, потом всё остальное. Эти два поля говорят, успешен ли запрос и является ли тело JSON, прежде чем вы дочитаете остальное.
Как выглядит здоровый JSON-ответ
Выполнение команды выше против тестового эндпоинта 27 октября 2026 года вернуло среди прочих такие заголовки:
| Заголовок | Значение | Что это говорит |
|---|---|---|
HTTP/2 | 200 | Запрос выполнен, сервер работает по HTTP/2 |
content-type | application/json | Вы получите JSON, а не HTML-страницу с ошибкой |
content-length | 567 | Это реальное тело, а не пустой ответ |
access-control-allow-origin | * | Браузерам разрешено читать этот ответ |
server | cloudflare | Перед исходным сервером стоит CDN |
alt-svc | h3=":443" | Для следующих запросов объявлен HTTP/3 |
Слой 5: заголовок, который ломает только вызовы из браузера
На этом слое пропадают часы, потому что именно здесь curl и браузер действительно расходятся. CORS проверяет браузер, а не сервер. Если Access-Control-Allow-Origin отсутствует, сервер всё равно возвращает полный и правильный 200, а браузер просто отказывается отдать тело вашему JavaScript.
Поэтому симптом такой: curl работает отлично, Postman работает отлично, а в консоли браузера написано, что запрос заблокирован политикой CORS. С сервером всё в порядке. В ответе просто не хватает одного заголовка.
Тестовый эндпоинт возвращает access-control-allow-origin: *, поэтому обычный fetch() работает с любого origin:
// 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 отдельно, потому что это другой ответ из другой ветки кода:
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-значения для кода:
{
"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, ENOTFOUND | 1 — DNS | DNS-запрос |
ECONNREFUSED, instant failure | 2 — Порт | Проверка портов |
| Запрос зависает, затем тайм-аут | 2 — Отфильтрованный порт | Проверка портов |
CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE | 3 — TLS | Проверка SSL |
| В браузере работает, в curl или Node нет | 3 — Неполная цепочка | Проверка SSL |
401, 429, 503 | 4 — Статус HTTP | Проверка HTTP-заголовков |
| В curl работает, в браузере заблокировано | 5 — CORS | Проверка HTTP-заголовков |
Unexpected token '<', "<!DOCTYPE "... is not valid JSON | 4 — HTML-страница с ошибкой | Проверка HTTP-заголовков |
| Нет полей или неверный тип | 6 — Тело ответа | curl … | jq |
Задержка немного выходит за рамки этой лестницы. Если все слои проходят, но ответы медленные, замерьте время кругового пути до хоста с помощью ping и проверьте, не узкое ли место само разрешение имён, тестом скорости DNS.
Проверьте реальные заголовки ответа любого эндпоинта
Посмотрите точный код статуса, content-type и заголовки CORS, которые возвращает API. Запрос идёт с серверной стороны, поэтому кэш браузера и расширения не искажают результат. Бесплатно, без регистрации.
Источники
Часто задаваемые вопросы
Как протестировать API-эндпоинт без Postman?
curl -s -D - -o /dev/null <url> покажет только заголовки ответа, curl -s <url> | jq позволит изучить JSON-тело, а curl -s -o /dev/null -w '%{http_code}' напечатает только код статуса. Для быстрой проверки совсем без терминала онлайн-проверка HTTP-заголовков выдаёт ту же информацию из браузера.Почему API работает в curl, но не работает в браузере?
Что означает код статуса 000 в curl?
-v, чтобы увидеть последний сработавший шаг.Как понять, что API не работает у всех, а не только у меня?
Почему API возвращает 200, но JSON-парсер выдаёт ошибку?
Что такое CORS preflight и когда он происходит?
Проверять DNS API через свой резолвер или публичный?
Означает ли открытый порт 443, что HTTPS-эндпоинт работает?
Похожие статьи
Связанные инструменты
Проверка HTTP-заголовков
Анализ HTTP заголовков ответа и оценка заголовков безопасности
Проверка DNS
Все DNS-записи прямо с серверов имён домена рядом с кешем Google и Cloudflare
Проверка портов
Тест доступности порта и определение работающих сервисов
Проверка SSL сертификата
Проверка действительности SSL сертификата и деталей безопасности
Пинг тест
Проверьте пинг, джиттер и потерю пакетов, пинг до 34 облачных регионов и пинг любого сайта или IP из 11 городов



