...

Теория

В соседней заметке я рассказывала про хрупкий код и приводила пример: делать запросы к внешнему http-сервису и не проверять код ответа.

Почему это проблема? Такой код работает нормально, пока внешний сервис отвечает нормально. Но когда внешний сервис сломается (а он обязательно сломается) – начнутся странные и неожиданные вещи. Вместо корректных данных в ожидаемом формате код весьма вероятно получит html-страничку с сообщением об ошибке. Попытка распарсить эти данные в лучшем случае приведет к ошибке парсинга, а в худшем – породит совершенно неправильные значения, которые программа будет пытаться обрабатывать как правильные.

Что делать? После любого http-запроса проверять статус ответа. Если запрос неуспешный – обрабатывать это явно каким-то подходящим образом.

Пример в подробностях

Допустим, мы используем в программе координаты МКС (международной космической станции); получаем их от сервиса http://open-notify.org.

По адресу http://api.open-notify.org/iss-now.json можно получить json с координатами МКС:

curl http://api.open-notify.org/iss-now.json -s
{"timestamp": 1633190837, "iss_position": {"longitude": "49.5884", "latitude": "43.2981"}, "message": "success"}

Далее парсим этот json и работаем с координатами.

Маленькая python-программа для демонстрации:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests

URL = "http://api.open-notify.org/iss-now.json"
# раскомментируй следующую строчку, чтобы эмулировать ситуацию с неуспешным запросом (статус 404 Not Found)
#URL = "http://open-notify.org/iss-now.json"

r = requests.get(url = URL)

# !!! не надо так. Прежде чем использовать ответ, убедись, что http-статус успешный
data = r.json()

latitude = data['iss_position']['latitude']
longitude = data['iss_position']['longitude']

print("Latitude:%s\nLongitude:%s" % (latitude, longitude))

Здесь проблема: если сервер координат сломается или поменяет свое API, код начнет вести себя странно.

Типичный случай поломки – сервис отвечает с кодом 502 и html-страничкой. В нашей программе это можно имитировать, если передать несуществующий url, в ответ на который мы получим статус 404. Раскомментируйте строчку с вторым присваиванием URL и посмотрите на поведение:

$ ./fragile-code-iss-01.py
Traceback (most recent call last):
  File "./fragile-code-iss-01.py", line 13, in <module>
    data = r.json()
  File "/usr/lib/python2.7/dist-packages/requests/models.py", line 892, in json
    return complexjson.loads(self.text, **kwargs)
  File "/usr/lib/python2.7/json/__init__.py", line 339, in loads
    return _default_decoder.decode(s)
  File "/usr/lib/python2.7/json/decoder.py", line 364, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/lib/python2.7/json/decoder.py", line 382, in raw_decode
    raise ValueError("No JSON object could be decoded")
ValueError: No JSON object could be decoded

Мы получаем исключение No JSON object could be decoded, по которому не очень понятно, что проблема происходит от неответа внешнего сервиса. Более того, парсинг json может происходить позже по коду, в другом методе, и по стектрейсу будет непонятно, откуда берутся невалидные данные.

Хорошая практика – проверять статус любого http-запроса, и если запрос окончился неуспешно – явно обрабатывать эту ситуацию подходящим образом. Пример:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
  
URL = "http://api.open-notify.org/iss-now.json"
# раскомментируй следующую строчку, чтобы эмулировать ситуацию с неуспешным запросом (статус 404 Not Found)
#URL = "http://open-notify.org/iss-now.json"

r = requests.get(url = URL)

if not r.ok:
    print("couldn't get current ISS coordinates, http status %s" % r.status_code)
    exit(1)

data = r.json()

latitude = data['iss_position']['latitude']
longitude = data['iss_position']['longitude']
  
print("Latitude:%s\nLongitude:%s" % (latitude, longitude))

В реальной программе вместо print и exit было бы логирование и какая-то подходящая обработка: перезапрос, использование дефолтных координат, запись статуса в базу данных и т.д.

А еще в json-ответе конкретно этого API есть поле message, которое обычно равно success. Но если может быть успех, может быть и что-то другое. Поэтому хорошо бы проверять и это поле тоже:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
  
URL = "http://api.open-notify.org/iss-now.json"

r = requests.get(url = URL)

if not r.ok:
    print("couldn't get current ISS coordinates, http status %s" % r.status_code)
    exit(1)

data = r.json()

if data['message'] != 'success':
    print("couldn't get current ISS coordinates, server message: '%s'" % data['message'])
    exit(2)


latitude = data['iss_position']['latitude']
longitude = data['iss_position']['longitude']
  
print("Latitude:%s\nLongitude:%s" % (latitude, longitude))

Протестировать этот вариант не получится, простого способа получить что-то кроме success от этого API я не знаю.

Итого

Внешние сервисы ломаются; код должен быть к этому готов.

Ссылки

Связанные заметки на SiliciumC

Внешние ссылки