С завидным постоянством в тематических каналах и чатах возникают вопросы про 429 и 500 ошибки при получении данных из API WB или OZ. Коллеги интересуются, нет ли особого параметра, секретного метода или “золотого” часа, когда гарантированно можно получить данные без ошибок. Увы, нет. А техподдержка отвечает всегда примерно так: "на метод высокая нагрузка, настройте ретраи на своей стороне". Поделюсь своим опытом решения этой проблемы, который применяется в системе аналитики продаж через Wildberries и Ozon. Напомню, небольшой обзор своей системы WBOZYA-dash я делал в предыдущей статье.

Очевидный прием

Самый очевидный путь решения проблемы частых ошибок 429 и 50x - относиться как к штатной ситуации и соответственно обрабатывать программной логикой. А логика тут одна - снова и снова делать запрос. Только чтобы не попасть под какие-либо санкции со стороны API, желательно ввести самоцензуру - паузы между запросами, ограничение на количество перезапросов.

Функции опроса API с обработкой ошибок может выглядеть так:

def get_oz(url, heads, params, max_retries=5, timeout=30):
    result = {}
    for retries in range(1, max_retries + 1):  
        
        try:
            response = requests.post(url, headers=heads, json=params, timeout=timeout)
        except requests.RequestException as e:
            print(f'!!! Сетевая ошибка в попытке {retries}: {e}!!!')
            time.sleep(2 * retries)
            continue  
                
        # Код 200 получен правильный ответ		
        if response.status_code == 200:
            return response.json()
            
        # Код 429 - Много запросов, делаем умную паузу и снова запрашиваем
        elif response.status_code == 429:	
            pause_time = 2 ** retries
            
            # X-Ratelimit-Retry - фишка WB, но может и OZ когда нить введет
            if 'X-Ratelimit-Retry' in response.headers:
                pause_time = int(response.headers['X-Ratelimit-Retry']) + 5
            print(f'get_oz: ! -Много запросов- в попытке {retries}. Пауза {pause_time} сек')
            time.sleep(pause_time)
        
        # При ошибке 5xx через паузу уходим на следующую попытку                    
        elif 500 <= response.status_code < 600:
            pause_time = 2 ** retries
            print(f'get_oz: ! -Ошибка {response.status_code}- в попытке {retries}. Пауза {pause_time} сек')
            time.sleep(pause_time)
        
        # Если другая ошибка - выходим из цикла!                
        else:
            print(f'get_oz: !!! Ошибка получения данных по API! code: {response.status_code}')
            return None
    

    print(f'get_oz: !!! Все {max_retries} попытки провалились')
  
    return None

Стоит отметить, что эта функция только на один запрос. То есть цикл с перебором страниц через offset или page будет на уровень выше в основной процедуре логики получения данных из API. Обычно 5 попыток хватает, если нет - то, видимо, на сервере серьезная перегрузка, и брать его измором "здесь и сейчас" наверняка не имеет смысла. Стоит возобновить запросы сильно попозже - например, через час. Как это сделать - покажу ниже по тексту.

Неочевидный прием

А пока расскажу о неочевидном пути решения: уменьшать объем получаемой информации. Логика тут такая при меньшем объеме данных, меньше и вероятность получить ошибку. Например, если запрашивать, скажем, транзакции OZ за неделю, в ответе будет 10-20 тысяч строк, а метод API все равно отдает их по 1000, то есть надо будет запросить 20 или более страниц. И вот где-то на 16-ой мы получаем ошибку и скаченные ранее 15 тысяч строк идут в /dev/null. Надо все заново перезапрашивать.

Предлагаю применять следующую тактику: запросить поменьше, сохранить полученные данные и запросить следующую партию. В том же примере про транзакции OZ, если за раз запрашивать транзакции только за один день, а всю неделю получить в цикле перебором семи независимых самостоятельных запросов.

Пример кода для запроса и сохранения транзакций по дням:

date_from = today() - timedelta(days=7)
date_to = today() - timedelta(days=1)
current_date = date_from
delta = timedelta(days=1)

#перебираем дни от date_from до date_to
while current_date <= date_to:  
	transactions_oneday = pd.DataFrame([])           
	url = "https://api-seller.ozon.ru/v3/finance/transaction/list"
	ipage = 1
    all_pages_data = []
	while True:
		data = {
			"filter": {
				"date": {
					"from": str(current_date) + "T00:00:00.000Z",
					"to": str(current_date) + "T23:59:59.999Z"
				},
			},
			"page": ipage,
			"page_size": 1000
			}
		dd = []
		apioz_response = get_oz(url, headers, data, 5)
		if len(apioz_response) > 0 :
            dd = apioz_response['result']['operations']
            all_pages_data.extend(dd)
		else:
			print(f'!!! Пустой ответ от API')	

        # если на странице меньше 1000 строк - она последняя, выходим из цикла    
        if len(dd) < 1000: break

        ipage +=1
            
    transactions_oneday = pd.DataFrame(all_pages_data)        
    #сразу экспортируем в базу, что получили из API
	export_to_db('oz_transactions', transactions_oneday)

    #переходим к следующему дню
	current_date += delta	

Получая 1-3 страницы данных, вероятность словить ошибку очевидно меньше, чем 15-20 страниц. Правда, это будет уже 4 или 5 уровень циклирования, но все же мой опыт показывает, что такой подход эффективен.

Костыль

Увы, очевидный и не очевидные пути все еще не дают полной гарантии получения информации из API. Сервис может просто не работать и измором и сотнями ретраев его не поднять. Тогда вступает черед следующего пути - назову его "костыль" =)

Если опрос API по расписанию где-то в 6-7 утра в итоге после всех ретраев так и не позволил загрузить целостный массив данных, что обычно делаем - загружаем в ручную. Так почему бы не запрограммировать и это. Написать еще одну процедуру, которая также периодически будет анализировать результаты последнего запуска нужной процедуры опроса API, и если она завершилась с ошибкой.- запускать ее снова. Или анализировать целостность сохраненных в БД данных, и перезапрашивать чего не хватает, например. транзакций за четверг.

Запускать такой мониторинг очевидно надо после завершения штатной работы целевой процедуры. Крутить его круглые сутки, наверно, особого смысла тоже нет. Лучшая тактика: запуск раз в час с 8 до 12 часов если основной опрос API происходит в 6--7 утра.

Заключение

Не скажу, что после внедрения всех этих решений, мне теперь совсем не приходится в ручную запускать опрос API, но все же делать это приходится значительно реже. В подавляющем большинстве система отрабатывает экспорт данных из API МП штатно. Но и специфика моей системы такова, что получение данных происходит всего один раз в сутки. Возможно, если у вас логика бизнес-процессов подразумевает более частый опрос API, то и нештатные ситуации будут встречаться чаще.