Hi, I need to send an HTTP POST request with a big binary blob (up to 10MB). I’m doing this from Elixir via an HTTP client library, but at the end of the day the code calls ssl:send/2
. This is causing a memory usage spike which forces me to turn down the concurrency since I’m running on limited memory. I traced how my HTTP client uses the ssl
module and wrote it down in code (see below). The interesting thing is that if I manually chunk the binary, the memory spike goes away. Please excuse the fact that it’s in Elixir, but I’m sure you’ll get the gist of it:
defmodule SslTest do
def run() do
host = "localhost"
port = 4040
method = "POST"
path = "/test/text"
content_length = 10 * 1024 * 1024
ssl_opts = [verify: :verify_none]
body = String.duplicate("-", content_length)
chunks = [body]
# chunks = chunk_binary(body, 1024 * 10)
IO.puts("waiting 3 seconds...")
Process.sleep(3000)
headers = [
{"host", "#{host}:#{port}"},
{"content-type", "text/plain"},
{"content-length", content_length}
]
{:ok, socket} =
:ssl.connect(
String.to_charlist(host),
port,
ssl_opts(host, ssl_opts)
)
request = [
"#{method} #{path} HTTP/1.1\r\n",
Enum.map(headers, fn {name, value} -> "#{name}: #{value}\r\n" end),
"\r\n"
]
:ok = :ssl.send(socket, request)
for chunk <- chunks do
:ok = :ssl.send(socket, chunk)
end
Process.sleep(500)
{:ok, response} = :ssl.recv(socket, 0, 0)
:ok = :ssl.close(socket)
IO.puts(response)
end
def ssl_opts(hostname, opts) do
[
server_name_indication: String.to_charlist(hostname),
versions: [:"tlsv1.3", :"tlsv1.2"],
depth: 4,
secure_renegotiate: true,
reuse_sessions: true,
packet: :raw,
mode: :binary,
active: false,
ciphers: [
%{cipher: :aes_256_gcm, key_exchange: :any, mac: :aead, prf: :sha384},
%{cipher: :aes_128_gcm, key_exchange: :any, mac: :aead, prf: :sha256},
%{cipher: :chacha20_poly1305, key_exchange: :any, mac: :aead, prf: :sha256},
%{cipher: :aes_128_ccm, key_exchange: :any, mac: :aead, prf: :sha256},
%{cipher: :aes_128_ccm_8, key_exchange: :any, mac: :aead, prf: :sha256},
%{
cipher: :aes_256_gcm,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :sha384
},
%{cipher: :aes_256_gcm, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha384},
%{
cipher: :aes_256_ccm,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :default_prf
},
%{
cipher: :aes_256_ccm_8,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :default_prf
},
%{
cipher: :chacha20_poly1305,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :sha256
},
%{
cipher: :chacha20_poly1305,
key_exchange: :ecdhe_rsa,
mac: :aead,
prf: :sha256
},
%{
cipher: :aes_128_gcm,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :sha256
},
%{cipher: :aes_128_gcm, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256},
%{
cipher: :aes_128_ccm,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :default_prf
},
%{
cipher: :aes_128_ccm_8,
key_exchange: :ecdhe_ecdsa,
mac: :aead,
prf: :default_prf
},
%{cipher: :aes_256_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha384},
%{cipher: :aes_256_gcm, key_exchange: :dhe_dss, mac: :aead, prf: :sha384},
%{
cipher: :aes_256_cbc,
key_exchange: :dhe_rsa,
mac: :sha256,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :dhe_dss,
mac: :sha256,
prf: :default_prf
},
%{cipher: :aes_128_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha256},
%{cipher: :aes_128_gcm, key_exchange: :dhe_dss, mac: :aead, prf: :sha256},
%{
cipher: :chacha20_poly1305,
key_exchange: :dhe_rsa,
mac: :aead,
prf: :sha256
},
%{
cipher: :aes_128_cbc,
key_exchange: :dhe_rsa,
mac: :sha256,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :dhe_dss,
mac: :sha256,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :ecdhe_ecdsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :ecdhe_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :ecdh_ecdsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :ecdh_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :ecdhe_ecdsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :ecdhe_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :ecdh_ecdsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :ecdh_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :dhe_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :dhe_dss,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :dhe_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :dhe_dss,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :rsa_psk,
mac: :sha256,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :rsa_psk,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :rsa_psk,
mac: :sha,
prf: :default_prf
},
%{cipher: :rc4_128, key_exchange: :rsa_psk, mac: :sha, prf: :default_prf},
%{
cipher: :aes_256_cbc,
key_exchange: :srp_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_256_cbc,
key_exchange: :srp_dss,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :srp_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :aes_128_cbc,
key_exchange: :srp_dss,
mac: :sha,
prf: :default_prf
},
%{cipher: :aes_256_cbc, key_exchange: :rsa, mac: :sha256, prf: :default_prf},
%{cipher: :aes_128_cbc, key_exchange: :rsa, mac: :sha256, prf: :default_prf},
%{cipher: :aes_256_cbc, key_exchange: :rsa, mac: :sha, prf: :default_prf},
%{cipher: :aes_128_cbc, key_exchange: :rsa, mac: :sha, prf: :default_prf},
%{cipher: :"3des_ede_cbc", key_exchange: :rsa, mac: :sha, prf: :default_prf},
%{
cipher: :"3des_ede_cbc",
key_exchange: :ecdhe_ecdsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :"3des_ede_cbc",
key_exchange: :ecdhe_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :"3des_ede_cbc",
key_exchange: :dhe_rsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :"3des_ede_cbc",
key_exchange: :dhe_dss,
mac: :sha,
prf: :default_prf
},
%{
cipher: :"3des_ede_cbc",
key_exchange: :ecdh_ecdsa,
mac: :sha,
prf: :default_prf
},
%{
cipher: :"3des_ede_cbc",
key_exchange: :ecdh_rsa,
mac: :sha,
prf: :default_prf
},
%{cipher: :des_cbc, key_exchange: :dhe_rsa, mac: :sha, prf: :default_prf},
%{cipher: :des_cbc, key_exchange: :rsa, mac: :sha, prf: :default_prf},
%{
cipher: :rc4_128,
key_exchange: :ecdhe_ecdsa,
mac: :sha,
prf: :default_prf
},
%{cipher: :rc4_128, key_exchange: :ecdhe_rsa, mac: :sha, prf: :default_prf},
%{cipher: :rc4_128, key_exchange: :ecdh_ecdsa, mac: :sha, prf: :default_prf},
%{cipher: :rc4_128, key_exchange: :ecdh_rsa, mac: :sha, prf: :default_prf},
%{cipher: :rc4_128, key_exchange: :rsa, mac: :sha, prf: :default_prf},
%{cipher: :rc4_128, key_exchange: :rsa, mac: :md5, prf: :default_prf}
]
]
|> Keyword.merge(opts)
end
def chunk_binary(str, size \\ 102_400, acc \\ []) do
case String.split_at(str, size) do
{slice, ""} -> Enum.reverse([slice | acc])
{slice, rest} -> chunk_binary(rest, size, [slice | acc])
end
end
end
(I wanted to upload a screenshot from Observer here, but I’m not yet allowed to post images, sorry!)
The data that I’m sending over the wire is actually JSON as iodata, but one of the values is this big binary blob encoded with base64.
I don’t have much experience with working with sockets directly, nor with the ssl
module, but my assumption was that this would just work without me needing to chunk it manually. What am I missing here?
EDIT: This is a cross-post from the Elixir Forum. Here’s the original thread.