Пример хрупкого кода: http-запрос без проверки статуса ответа
Теория
В соседней заметке я рассказывала про хрупкий код и приводила пример: делать запросы к внешнему 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
Внешние ссылки
- https://www.geeksforgeeks.org/get-post-requests-using-python/ – пример python-программы с обращением к maps.googleapis.com