Ошибка 405 Method Not Allowed в веб-скрапинге: причины и решения

Ответ 405 Method Not Allowed означает, что сервер нашёл ваш URL, но отклонил отправленный вами HTTP-метод. В скрапинге на деле часто происходит не это. Некоторые серверы отвечают автоматизированным клиентам кодом 405, поэтому ваш скрипт падает на странице, которая без проблем загружается в Chrome.
Это руководство для разработчиков, которые отлаживают скрапер на Python. Из него вы узнаете, как примерно за две минуты отличить один случай от другого и что делать в каждом из них.
Что означает ошибка 405 Method Not Allowed
405 — это ошибка клиента. Сервер нашёл ресурс и понял запрос, но использованный вами метод отсутствует в списке методов, которые этот ресурс принимает.
RFC 9110, который в июне 2022 года заменил RFC 7231, требует, чтобы серверы отправляли заголовок Разрешить с каждым ответом 405, перечисляя в нём поддерживаемые методы. Почти никто при отладке скрапера его не читает. А ведь это самый быстрый способ отличить настоящую ошибку метода от отказа в обслуживании.
Один и тот же код состояния отображается по-разному в зависимости от серверного ПО:
- 405 Method Not Allowed
- HTTP Error 405 – Method Not Allowed
- 405 Not Allowed
- The requested method POST is not allowed for the URL
- HTTP verb used to access this page is not allowed
- This page isn’t working – HTTP ERROR 405
На уровне протокола все они означают одно и то же.
Почему ваш скрапер получает 405, а браузер нет
Ошибка 405 в скрапере возникает по одной из трёх причин. Каждая требует своего решения, поэтому угадывать — значит терять время.
| Причина | Что вы видите | Как подтвердить |
|---|---|---|
| Метод действительно неверный | 405 на API или эндпоинте формы, каждый раз | Разрешить заголовок перечисляет метод, который вы не использовали |
| Сервер отклоняет ваш клиент | 405 на обычной странице, которая должна принимать GET | Тот же URL открывается в Chrome |
| Отклонение CORS preflight-запроса | 405 на запрос OPTIONS от инструментов автоматизации браузера | Только при внедрённых кросс-доменных запросах |
Почему сервер возвращает 405 вместо 403
На обычной странице GET, очевидно, разрешён, поэтому 405 там на самом деле не про метод. Некоторые защитные слои отвечают не-браузерным клиентам 405 вместо 403, потому что это говорит вам меньше. 403 сообщает, что вас распознали и отказали. 405 отправляет вас переписывать методы запросов.
Обычно клиент на Python выдаёт себя не строкой User-Agent. Его выдаёт всё, что находится под ней.
- TLS-рукопожатие.
запросысогласовывает TLS через OpenSSL. Его список шифров, порядок расширений и поддерживаемые группы не совпадают с Chrome, и эта комбинация достаточно стабильна для фингерпринтинга. Хэши JA3 и JA4 описывают именно это. - Версия протокола. Chrome общается с большинством крупных сайтов по HTTP/2 или HTTP/3.
запросыговорит только по HTTP/1.1. Это разделяет их ещё до того, как будет прочитан хоть один заголовок. - Настройки HTTP/2. Клиенты, которые всё же используют HTTP/2, отправляют собственные значения
SETTINGSи размеры окон, и они различаются от библиотеки к библиотеке. - Порядок заголовков. Браузеры отправляют заголовки в постоянном порядке.
запросыдобавляет собственные заголовки по умолчанию в своём порядке, который не совпадает ни с одним браузером.
Поэтому подмена User-Agent на Chrome иногда ничего не меняет. Вы исправили один сигнал из четырёх.
Когда метод действительно неверный
Настоящая версия этой ошибки появляется, когда вы обращаетесь к эндпоинту, взятому из DevTools, а не к странице. Эндпоинты поиска, обработчики пагинации и формы входа обычно принимают только POST.
Откройте вкладку Network, найдите неудавшийся запрос и посмотрите на колонку Method. Если там указан POST, а ваш код отправляет GET, вопрос закрыт. Ничего из диагностики блокировок здесь не применимо.
Как диагностировать ошибку 405 за две минуты
Выполняйте эти шаги по порядку. Остановитесь, как только один из них даст вам ясный ответ.
1. Прочитайте заголовок Разрешить заголовок. Самая быстрая проверка, и обычно она сама снимает вопрос.
python
import requests
url = "https://example.com/api/search"
response = requests.options(url, timeout=10)
print(response.status_code)
print(response.headers.get("Allow"))Если в ответе перечислен GET, а ваш GET был отклонён, проблема не в методе. Если указан только POST, проблема именно в нём.
2. Откройте URL в браузере. Работает в Chrome, не работает в вашем скрипте, сеть одна и та же: разница в вашем клиенте.
3. Проверьте, когда именно происходит сбой. Сбой на первом же запросе указывает на то, как сформирован запрос. Сбой после пятидесяти успешных запросов указывает на ограничение частоты запросов.
4. Посмотрите на заголовки ответа. cf-ray, server: cloudflare или заголовки Akamai означают, что ответил защитный слой, а не само приложение.
5. Подождите пятнадцать минут и отправьте тот же запрос ещё раз, ничего не меняя. Если он сработал, вы упёрлись во временное ограничение. Снизьте темп, а не переписывайте код.
| Диагностика | Иди |
|---|---|
Разрешить указывает метод, который вы не использовали | Решение 1 |
| Открывается в Chrome, но в вашем скрипте не работает с первого же запроса | Решение 2 |
| Работает, затем перестаёт после N запросов | Решение 3 |
| Проходит само по себе после паузы | Решение 3, плюс снизьте частоту запросов |
Решение 1: отправляйте метод, который ожидает эндпоинт
Просто, когда заголовок Разрешить уже подсказал вам, что отправлять. Воспроизведите запрос, который показали DevTools, включая payload и тип содержимого.
python
import requests
url = "https://example.com/api/search"
payload = {"query": "laptops", "page": 1}
response = requests.post(url, json=payload, timeout=10)
print(response.status_code)Здесь многих подводят редиректы. Если запрос проходит через 301 или 302, некоторые клиенты по пути превращают POST в GET, так что по URL, который вы написали, метод верный, а по URL, куда вы в итоге попали, нет. Выведите response.history и посмотрите, как выглядел итоговый запрос, прежде чем винить эндпоинт.
Решение 2: отправляйте полный запрос
Если эндпоинт принимает ваш метод, но отвергает ваш клиент, значит, сам запрос неполный. Браузер отправляет с десяток заголовков при каждом переходе. Голый requests.get() отправляет четыре.
python
import requests
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1",
}
session = requests.Session()
session.headers.update(headers)
response = session.get("https://example.com/", timeout=10)
print(response.status_code)Использовать Сессия вместо отдельных вызовов. Она сохраняет файлы cookie между запросами и переиспользует соединение, как это делает браузер. Независимые запросы, каждый из которых открывает новое соединение и не несёт файлов cookie, выглядят странно на любом сайте с авторизацией.
Задавайте Referer только там, где он соответствовал бы действительности. Запрос, пришедший с реферером со страницы, которая на него не ссылается, хуже, чем запрос вообще без реферера.
Если полный набор заголовков ничего не меняет, остаётся разница в TLS и протоколе, описанная выше. curl_cffi и httpx с http2=True частично закрывают этот разрыв. А браузер для скрейпинга закрывает остальное, но стоит вам скорости, поскольку это настоящий браузерный движок.
Решение 3: распределите запросы между большим числом IP-адресов
Если 405 появляется только после серии успешных запросов или сам исчезает после паузы, причина в объёме запросов с одного адреса. Заголовки здесь ни при чём. Их переписывание не поможет.
Сначала замедлитесь. Добавьте задержку между запросами и не завышайте параллелизм. Сайты публикуют свои ограничения частоты запросов чаще, чем принято думать, и оставаться в их рамках дешевле, чем обходить их инженерными средствами.
Затем распределите нагрузку. Ротационные резидентские прокси выдают отдельный IP на каждый запрос или на каждую сессию, так что лимит на один адрес перестаёт быть потолком для всей задачи. Работе, где сессию нужно удерживать, например всему, что находится за авторизацией, лучше подходит закреплённая сессия или статический IP, потому что смена адреса посреди сессии сама запускает повторную проверку.
python
proxies = {
"http": "http://YOUR_USERNAME:YOUR_PASSWORD@PROXY_HOST:PROXY_PORT",
"https": "http://YOUR_USERNAME:YOUR_PASSWORD@PROXY_HOST:PROXY_PORT",
}
response = session.get(url, proxies=proxies, timeout=15)
print(response.status_code)Качество IP здесь важно по практической причине. Адреса, уже отмеченные где-то ещё, приходят вместе с этой репутацией, поэтому ротация через пул низкого качества может давать сбои чаще, чем один чистый адрес. NodeMaven фильтрует свой пул до того, как IP попадут к вам.
Логика повторных запросов, которая не расходует трафик впустую
Большинство скраперов повторяют любой ответ, отличный от 200, одинаково. Для 405 это неверное поведение по умолчанию. Реальное несовпадение метода будет падать одинаково всегда, а каждая попытка стоит трафика.
Классифицируйте ошибку один раз при первом сбое, а затем действуйте по результату.
python
import time
import requests
def fetch(session, url, proxy_pool, max_attempts=3):
for attempt in range(max_attempts):
proxies = proxy_pool[attempt % len(proxy_pool)]
response = session.get(url, proxies=proxies, timeout=15)
if response.status_code != 405:
return response
allowed = response.headers.get("Allow", "")
if allowed and "GET" not in allowed:
raise ValueError(f"GET not supported here. Allowed: {allowed}")
time.sleep(2 ** attempt)
return NoneНикогда не повторяйте запрос при подтверждённом несовпадении метода. Меняйте IP между попытками, а не повторяйте с одного и того же адреса. Делайте паузу с нарастающей задержкой вместо того, чтобы долбить сервер, потому что временному ограничению нужно время, а не ещё один запрос.
405 против 403, 404, 401 и 415
Их постоянно путают, поскольку симптом один и тот же. Ваш запрос не прошёл.
| Код | Ресурс существует | Запрос корректно сформирован | Метод разрешён | Доступ разрешён | Обычное значение при скрапинге |
|---|---|---|---|---|---|
| 400 Неверный запрос | n/a | Нет | n/a | n/a | Некорректное тело запроса или неверные параметры |
| 401 Не авторизовано | Да | Да | Да | Нужна авторизация | Отсутствует токен или истекла сессия |
| 403 Запрещено | Да | Да | Да | Нет | Вас идентифицировали и отказали в доступе |
| 404 Не найдено | Нет | Да | n/a | n/a | Неверный URL или ресурс скрыт от вас |
| 405 Method Not Allowed | Да | Да | Нет | Да | Неверный метод или замаскированный отказ |
| 415 Unsupported Media Type | Да | Да | Да | Да | Неверно Content-Type в POST-запросе |
Ошибки 403 и 405 на одном сайте на практике часто означают одно и то же. Просто 403 признаёт это открыто.
Как ошибка 405 проявляется в разных инструментах
| Инструмент | Что вы видите | Проверьте сначала |
|---|---|---|
| запросы | response.status_code == 405, исключение не выбрасывается | Проверяете ли вы код статуса вообще |
| httpx | То же самое, если вы не вызываете raise_for_status() | Включить http2=True |
| Scrapy | Ответ отбрасывается по умолчанию, 405 отсутствует в handle_httpstatus_list | DOWNLOAD_DELAY и CONCURRENT_REQUESTS_PER_DOMAIN |
| Playwright | response.status() в ответе на навигацию | Упала ли сама страница или фоновый XHR-запрос |
| curl | Только тело ответа, если вы не передадите -i или -в | Беги curl -X OPTIONS -i чтобы прочитать Разрешить заголовок |
Отдельно стоит сказать о Scrapy. Он отбрасывает ответы с кодами, отличными от 2xx, ещё до запуска вашего парсера, поэтому паук (spider), который ничего не возвращает, может на самом деле получать 405, которых вы не видите. Добавьте 405 в handle_httpstatus_list на время отладки, и ответ дойдёт до вашего callback.
Когда правильный ответ: остановиться
Иногда 405 означает, что сайт не хочет видеть автоматизированный трафик, и правильная реакция в этом случае — оставить его в покое.
Проверить robots.txt и условия использования, прежде чем тратить на него больше времени. Поищите официальный API: он, как правило, быстрее и стабильнее, чем скрапинг фронтенда. Сбор данных должен оставаться в рамках закона и правил самой платформы, и всё описанное здесь предполагает работу с публично доступными данными на сайтах, где их сбор разрешён.




