Жуткая история одного пользователя
На днях я столкнулся с весьма загадочным и мистическим явлением в лучших традициях сериала «Секретные материалы». Аж кровь в жилах застыла. В общем, удалили пользователя из системы, а душа его осталась плутать по закоулкам и пугать все входящие API запросы. Да так, что сторонние системы, которые эти запросы слали, в ужасе шарахались и бросали NullPointerException. Если не хотите спать сегодня ночью, то смело заглядывайте сюда и узрите сию жуткую историю.
Произошло это, как и куча всякого другого дерьма, в 2020 году.
У меня была интеграция, разработкой которой я занимался, и сторонняя система, API которой дергала моя интеграция. Чтобы не путаться, давайте назовем систему Системой, а интеграцию – ни много ни мало, Интеграцией.
Интеграция моя отвечала за то, чтобы получать на вход email и создавать в Системе пользователей с этим email и другими заданными атрибутами. Если же пользователь уже существовал в Системе, Интеграция должна была получить его по айди и присвоить эти самые атрибуты. Проблема заключалась в том, что айди пользователя нам заранее неизвестен, и никаких способов получения пользователя по email Система не предоставляла. Поэтому мне приходилось итерироваться по всем пользователям в Системе (благо, их там довольно мало, и сильно много их стать не должно) и искать совпадающий email:
users.stream().filter(user -> user.getAccount().getEmail().equals(email)).findFirst();
Особых проблем это не доставляло, и Система возвращала ответ в следующем формате:
"Users": [{
"id": 42,
"displayName": "Artem Krutov",
"account": {
"id": 42,
"username": "artem.krutov",
"email": "user@email.com"
}
},
{
...
}]
Как видно, возвращался нам массив пользователей, и email каждого из них был доступен во внутренней структуре account
.
Тщательно протестировав Интеграцию и прогнав несколько реальных сценариев, я отправился на заслуженный отдых.
И тогда это началось…
Шутка, началось все тогда, когда я через интерфейс Системы удалил созданного тестового юзера, чтобы он не путался под ногами, да и в целом, чтобы его никто не видел. А вот когда я его удалил… душа его осталась бродить по длинным и темным коридорам системы.
До поры до времени все было хорошо. Но когда один из пользователей был удален в Системе, и его темная душа начала пугать мои запросы, моя Интеграция начала себя странно вести и бросаться ошибками. Проблема была все в той же строке с итерированием:
users.stream().filter(user -> user.getAccount().getEmail().equals(email)).findFirst();
А все потому, что душа удаленного пользователя все еще присутствовала в ответе Системы:
{
"id": 42,
"displayName": "Artem Krutov",
"account": {
"id": 42,
"username": "deleted-42",
"email": null
}
}
Видите? Данные пользователя обнулились, но он все равно возвращается в ответе. И, так как Интеграция предполагает, что раз мы нашли пользователя, то должен быть и его уникальный идентификатор в виде email, а его не оказалось, падал NPE:
java.lang.NullPointerException: null
Это выглядит довольно стремно и костыльно. Зачем возвращать мертвого обнуленного юзера, с которым никак нельзя работать? Но из всего этого можно вынести простую мораль:
КОГДА ВЫ РАБОТАЕТЕ СО СТОРОННИМИ СИСТЕМАМИ, ОЖИДАЙТЕ ОТ НИХ ПОДСТАВ
Они могут изменить свое поведение и наказать вас в любой момент. Конечно, суть проблемы в том, что я не защитился от этой ошибки изначально. Если бы мой код предполагал, что подстава может случиться, он бы выглядел следующим образом:
if (users != null) {
return users.stream().filter(user -> {
if (user != null && user.getAccount() != null && user.getAccount().getEmail() != null) {
return user.getAccount().getEmail().equals(email);
}
return false;
}).findFirst();
}
return Optional.empty();
Всегда относитесь к сторонним системам с подозрением, ибо они в любой момент могут начать слать что-то неожиданное. Проверяйте полученные данные и отвергайте невалидные ответы. Обезопасьте себя от плохих ответов заранее, а не тратьте время на анализ логов, как это сделал я!
Понравилось? Подписывайтесь на меня в соцсетях!
Кошмар! Впервые вижу настолько костыльное решение для удаления) Системный аналитик во мне негодует, а воображение рисует все трудности отлавливания таких багов. Бррр
При сравнении данных в Java всегда лучше вызывать equals от той переменой, от которой есть уверенность что она не null. По крайней мере, я выработал эту привычку у себя. Конечно, это не спасет если аккаунт например null, но зачастую в 90% случаев помогает.
Это правильная практика, и такая конструкция действительно не приводила бы к ошибке в данном случае:
Спасибо за отзыв!