Сервис напрямую отправляет клиенту текст любой ошибки: какое свойство дизайна ошибок нужно обеспечить, чтобы внутренние детали не стали частью внешнего контракта?
Текст Error() должен быть пригоден только для диагностики и не содержать секретов, SQL, внутренних путей и других деталей реализации. Клиенту нельзя безусловно передавать err.Error(); внешний ответ нужно формировать отдельно, по контролируемой категории или коду ошибки.
В Go ошибка представлена интерфейсом с методом Error() string. Этот метод даёт текстовое описание для человека, но язык не разделяет автоматически диагностическое сообщение и публичное сообщение для клиента.
Механизмы wrapping и errors.Is/errors.As позволяют сохранять и исследовать внутреннюю причину. Это удобно для логирования и принятия решений, но одновременно означает, что ошибка может раскрывать больше сведений, чем допустимо во внешнем API.
Если HTTP-обработчик возвращает клиенту err.Error(), наружу могут попасть текст запроса, имя таблицы, адрес внутреннего сервиса, путь к файлу или данные, полученные от сторонней системы. Даже без явного секрета такая информация раскрывает устройство системы и становится неявной частью API.
Особенно опасны обёртки, которые добавляют контекст с потенциально чувствительными значениями. Форматирование ошибки через %w сохраняет причину для последующего анализа, но не делает её безопасной для показа пользователю.
Нужно разделить два канала: диагностический и публичный. В диагностическом канале сохраняют исходную ошибку и контекст, а на границе API преобразуют известные категории в заранее определённые публичные ответы.
Сам Error() у ошибки должен быть безопасным по содержанию: без секретов, персональных данных и значений, которые нельзя раскрывать. Однако одной безопасной строки недостаточно: обработчик всё равно не должен сериализовать произвольную ошибку, потому что сторонний тип может нарушить это правило.
Минимальный пример разделения внутренней причины и внешнего ответа:
В реальном приложении исходную ошибку логируют с контролем доступа, а клиенту возвращают отдельную структуру с публичным кодом и сообщением. Компромисс состоит в том, что клиент получает меньше диагностической информации, зато формат ответа стабилен и не раскрывает реализацию.
Следует также определить правила для логирования: не добавлять секреты в контекст ошибки, не логировать одну и ту же цепочку многократно и проверять, что данные из входного запроса не попадают в сообщение без необходимости.
Платёжный сервис получил ошибку от базы данных, содержащую фрагмент запроса и имя внутреннего узла. Первый вариант — вернуть клиенту err.Error(). Он прост, но раскрывает инфраструктуру и делает внутренний текст частью внешнего API.
Второй вариант — всегда возвращать одну строку вроде «внутренняя ошибка». Это безопасно, но клиент не сможет отличить временную недоступность от некорректного запроса и выбрать правильную стратегию повторения.
Выбранное решение — классифицировать ошибку внутри сервиса, вернуть стабильный публичный код, например temporary_unavailable, а исходную цепочку сохранить только для защищённого журнала. Такой подход сохраняет полезную семантику для клиента, не раскрывая конкретную причину и детали реализации.
Дополнительный вопрос 1: Достаточно ли удалить секрет из Error(), если ошибка поддерживает wrapping?
Нет. Если ошибка оборачивает исходную причину через %w или Unwrap, доверенный код всё ещё может получить её через errors.Unwrap, errors.Is или errors.As. Поэтому безопасность должна обеспечиваться границей публикации: внешний слой не сериализует произвольную ошибку и не предоставляет клиенту её цепочку.
Дополнительный вопрос 2: Нужно ли скрывать от клиента все различия между ошибками?
Нет, но различия должны быть явно спроектированы. Клиенту можно сообщать стабильные категории: например, «некорректный запрос», «не найдено» или «временная ошибка». Нельзя использовать для этого случайный текст внутренних ошибок, поскольку он меняется вместе с реализацией и может раскрыть лишние сведения.
Дополнительный вопрос 3: Можно ли считать любой текст ошибки безопасным, если он не содержит паролей?
Нет. Чувствительными могут быть персональные данные, токены, идентификаторы внутренних объектов, SQL-тексты, пути файлов и адреса инфраструктуры. Кроме прямой утечки, такие сообщения могут позволить клиенту отличить существующий объект от отсутствующего или подобрать внутреннюю структуру системы.