Go: не используйте http.Server.Serve и http.Server.ServeTLS одновременно

Эта заметка будет очень короткой. Но надеюсь, она кому-то спасёт несколько часов жизни.

У меня был код. К счастью, это было в тесте, а не в боевом коде, поэтому никто не пострадал.

Код создавал http.Server, запускал две гороутинки для обслуживания входящих соединений:

go func() {
                srvr.Serve(p)
}()

go func() {
                srvr.ServeTLS(e, "", "")
}()

Ну и дальше создавал клиента, делал к серверу обращения (HTTP GET) попеременно используя http и https ну и чего-то там проверял.

Всё прекрасно работало. До обновления с go1.23.8 до go1.24.2, пришедшего с 42-й Федорой.

А потом перестало. Стало время от времени (но отнюдь не всегда) вываливать разнообразные ошибки. Например, вот такие: Get "https://127.0.0.1:46167/": unexpected EOF. Или такие: Get "https://127.0.0.1:34757/": write tcp 127.0.0.1:54770->127.0.0.1:34757: write: connection reset by peer. Или даже вот такие, совсем загадочные: Get "https://127.0.0.1:42447/": http2: client conn could not be establish. HTTP/2 там, разумеется никто не включал и не собирался. А иногда всё работало и тест проходил правильно.

Самое поганое, что ошибка была плавающей.

В общем, не буду грузить подробностями, как я эту ошибку ловил. Но итог такой. Хотя это нигде и не документировано, но одновременно использовать http.Server.Serve и http.Server.ServeTLS на одном и том же экземпляре сервера нельзя. Тот из них, кто успеет прокрутиться первым, чего-то там инициализирует внутри сервера, прежде, чем уйти в accept loop, и второй после этого ломается. Ломается всегда ServeTLS, не-TLS-овскому Serve вроде как пофигу.

Так что будьте осторожны, и надеюсь, что эта заметка сохранила вам несколько часов жизни :)

@apevzner
04.05.2025 22:06 UTC
Первоисточник

Комментарии

@lrrr11
04.05.2025 17:34 UTC
+4

Подробности того, как ловил ошибку, как раз и есть самое интересное.

А ещё это показательный пример работы с нейронкой (grok, в моём случае). Если скормить ей кусок кода из статьи и попросить найти проблемы, то она напишет кучу воды про обработку ошибок и т.п., но не про это. А если прямо спросить про гонку данных, то распишет во всех подробностях. Вот поэтому нейронки не заменят программистов, по крайней мере не в близкой перспективе. Потому что основной скилл хорошего программиста - это вовсе не умение что-то там написать. Это понимание того, что надо написать, в какую сторону копать и какие вопросы спрашивать. В том числе и у нейронки.

@apevzner
04.05.2025 17:53 UTC
+5

Подробности того, как ловил ошибку, как раз и есть самое интересное.

Понятное дело. Только пересказать это невозможно.

Сначала пытался отсекать лишнее. Но только беда в том, что как я не упрощал этот код, плавающая ошибка не уходила (может это и к лучшему, иногда бывает так, что плавающая ошибка всё время лезет, пока ей не займешься. А потом прячется).

Выяснил, что если вставить задержку в гороутинку, которая делает ServeTLS, то ошибка выскакивает всегда. А если в другую, то и вовсе не выскакивает.

С самого начала было понятно, что это какая-то гонка. Но сколько я ее не искал, найти не мог.

Посмотрел в код стандартной библиотеки, не делает ли она чего в фоне. Вроде нет, но она сложная, её сложно читать, поэтому 100% не было.

Возникла гипотеза, что может если создавать сервер и сразу делать к нему запрос, возможно внутри самого сервера что-то не успело еще проинициализироваться, и слишком ранние запросы сервер отрабатывает неправильно (в боевых-то условиях так редко бывает и особенно не заметно). Написал соответствующий тест, погонял - все ОК. Догадка не подтвердилась.

Поискал, где запускаются гороутинки в net.http и crypto.tls. Сравнил между версиями go. Никакой значимой разницы не обнаружил.

Потом пришла догадка, что может Serve и ServeTLS и не стоило бы одновременно запускать. Посмотрел еще раз попристальнее в код net.http - вижу, что они там на старте чего-то в Server накручивают под себя, в его глубинах. Так догадка перерасла в уверенность.

Ну а дальше просто, разбил Server на два, проверил - работает. Убрал всю вставленную в процессе отладки отладочную печать, еще раз проверил - работает. Ну и туды его, в commit :)

@rustler2000
04.05.2025 19:42 UTC
0

А go test -race не указывал на проблемы?

@apevzner
04.05.2025 20:49 UTC
0

Нет, не указывал.

@apevzner
04.05.2025 20:51 UTC
+1

Там вроде есть какая-то синхронизация, в инициализации этого самого http.Server. Т.е., там не классическая гонка, а "кто первый встал, того и тапки". Т.е., в зависимости от того, какая функция первая успеет прошмыгнуть в критическую секцию, результат получится разный. Но консистентный. Но при этом не подходящий для другой функции.

@pda0
05.05.2025 08:57 UTC
0

Да не похоже на гонку. Тем более тогда бы ломался бы не "всегда ServeTLS". "unexpected EOF" в HTTP намекает на то, что сервер некорректные данные отдаёт, например в TLS соединении - обычные незашифрованные.

06.05.2025 15:20 UTC
0

Вполне себе гонка: две горутины инициализируют одни и те же приватные поля структуры

Правда не уверен, что test -race такое ловит

07.05.2025 08:06 UTC
0

race может и не ловит, а вот линтер из vscode вполне. Но при гонке должна какая-то рандомизация быть. Т.е. иногда срабатывало бы наоборот, TLS работает, а в plain - шифрованная каша.

08.05.2025 10:28 UTC
0

race может и не ловит, а вот линтер из vscode вполне.

А Вы попробовали, или это - теоретическое умозаключение?

Т.е. иногда срабатывало бы наоборот, TLS работает, а в plain - шифрованная каша.

Там инициализируются какие-то внутренности, которые важны для TLS, а для plain не очень важны. Поэтому plain не очень ломается. Ну или, по крайней мере, мне в ограниченном числе тестов не удались заставить его сломаться.

12.05.2025 11:59 UTC
+1

А Вы попробовали, или это - теоретическое умозаключение?

Точных подробностей не назову, пол года уже прошло, но это было со мной. Защищал карту стандартным sync.Mutex, линтер ругнулся в какой-то момент, мол у вас потенциальный race condition. Был абсолютно уверен, что это ложное срабатывание, ведь я умею писать код с потоками. ;-)

Но варнинги висячие не люблю, полез разбираться что конкретно ему не нравится и обнаружил, что прав был таки линтер, а не я, был вариант, при котором шло незащищённое обращение к карте.

Ну или, по крайней мере, мне в ограниченном числе тестов не удались заставить его сломаться.

Ну, возможно количество тестов было очень ограниченным. Просто признак реальной гонки - когда результат непостоянен, то один, то другой. Собственно весь смысл гонки в этом.

12.05.2025 13:06 UTC
0

Результат был непостоянен, я ж написал

13.05.2025 07:37 UTC
0

Значит проглядел, тогда да, гонка.