crystal
crystal copied to clipboard
Cannot perform concurrent HTTPS GET requests from a single client session
The following code seems to always crash:
require "http/client"
client1 = HTTP::Client.new("example.com", tls: true)
client2 = client1
ch1 = Channel(Int32).new
ch2 = Channel(Int32).new
puts LibSSL::OPENSSL_VERSION
5.times do |i|
spawn do
response = client1.get "/"
ch1.send response.body.size
end
spawn do
response = client2.get "/"
ch2.send response.body.size
end
puts i
puts "size = #{ch1.receive}"
puts "size = #{ch2.receive}"
end
3.0.5
0
size = 1256
size = 1256
1
size = 1256
Invalid memory access (signal 11) at address 0x0
[0x55a32ed51f06] *Exception::CallStack::print_backtrace:Nil +118 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ed39bda] ~procProc(Int32, Pointer(LibC::SiginfoT), Pointer(Void), Nil) +330 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x7f7b3003daf0] ?? +140167063263984 in /lib/x86_64-linux-gnu/libc.so.6
[0x7f7b3009693c] ?? +140167063628092 in /lib/x86_64-linux-gnu/libc.so.6
[0x7f7b30097275] malloc +149 in /lib/x86_64-linux-gnu/libc.so.6
[0x7f7b30a55113] ?? +140167073845523 in /usr/lib/x86_64-linux-gnu/libssl.so.3
[0x7f7b30a524f5] ?? +140167073834229 in /usr/lib/x86_64-linux-gnu/libssl.so.3
[0x7f7b30a55e78] ?? +140167073848952 in /usr/lib/x86_64-linux-gnu/libssl.so.3
[0x7f7b30a543f0] ?? +140167073842160 in /usr/lib/x86_64-linux-gnu/libssl.so.3
[0x7f7b30a296ae] ?? +140167073666734 in /usr/lib/x86_64-linux-gnu/libssl.so.3
[0x7f7b30a35283] SSL_read +35 in /usr/lib/x86_64-linux-gnu/libssl.so.3
[0x55a32ee26eed] *OpenSSL::SSL::Socket+ +109 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee26d5e] *OpenSSL::SSL::Socket+ +78 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee26c86] *OpenSSL::SSL::Socket+ +54 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee198b2] *IO+ +1186 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee1d08a] *IO+ +170 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee78577] *HTTP::Client::Response::from_io?<IO+, Bool, Bool>:(HTTP::Client::Response | Nil) +103 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee60360] *HTTP::Client#exec_internal_single<HTTP::Request>:(HTTP::Client::Response | Nil) +80 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee600dd] *HTTP::Client#exec_internal<HTTP::Request>:HTTP::Client::Response +29 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee5fdd7] *HTTP::Client#exec<HTTP::Request>:HTTP::Client::Response +71 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee5fd76] *HTTP::Client#exec<String, String, Nil, Nil>:HTTP::Client::Response +22 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ee5fd57] *HTTP::Client#get<String>:HTTP::Client::Response +39 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ed3e661] ~procProc(Nil) +33 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ed9c0f9] *Fiber#run:(IO::FileDescriptor | Nil) +185 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x55a32ed396f6] ~proc2Proc(Fiber, (IO::FileDescriptor | Nil)) +6 in /home/quinton/.cache/crystal/crystal-run-test.tmp
[0x0] ???
The same works for non-HTTPS connections and also if two different HTTP::Client
s sharing the same TLS context are used:
tls_cxt = OpenSSL::SSL::Context::Client.new
client1 = HTTP::Client.new("example.com", tls: tls_cxt)
client2 = HTTP::Client.new("example.com", tls: tls_cxt)
Regardles of HTTPS, it is not safe to do this with an instance of HTTP::Client
and ensure correctness.
HTTP::Client
is not "fiber-safe" as-designed, as you have found out - but besides TLS negotiation problems, one fiber may receive the response of a totally different request started by another, or other permutations. So this is probably never a good idea.
Anyways - there are probably two ways to fix the segfault off the top of my head:
- Right now,
HTTP::Client.new
will lazy-connect. MeaningHTTP::Client.new
doesn't do anything until you call.get
or some other action. It could be changed so that it connects right away, ensuring only one fiber does the setup. - Some kind of mutex scheme around TLS handshake, which doesn't sound great.
Personally I would like to see (1). I think this is fine. I patch HTTP::Client
to do this anyways because I set up connection pools.
I think we should eventually change HTTP::Client
so that it's fiber-safe.
In the meantime, it would be great if the docs were be updated to state that HTTP::Client
is not fiber-safe. I made the same assumption (with the aim of reusing a TCP connection), and was similarly bitten.
Meta issue on refactoring HTTP::Client
: https://github.com/crystal-lang/crystal/issues/6011