1 min read

Ruby, Async and HTTP

Using async-http with HTTP/2
Ruby, Async and HTTP
Photo by Cookie the Pom / Unsplash

The Async library for ruby is pure magic. Asynchronously running arbitrary ruby code, without having to deal with the pesky coloured functions of other languages, is beautiful and powerful.

I haven't explored the sister project, async-http, much at all until recently. I'm building a rails project which has both a web interface and an API. Posts to the API cause a cascade of model updates which trigger Turbo Streams refreshes in the browser.

A client gem makes the API calls. As they're all simple fire-and-forget POST messages, they're ideal for wrapping in async. Something like this:

def post(document, to:)
  Async do
    internet = Async::HTTP::Internet.new
    body = document.to_json
    endpoint = to
    internet.post(endpoint, headers, body)
  ensure
    internet.close
  end
end

Don't do this

This worked really well until my web server decided to negotiate HTTP/2 connections and the client died after the first POST.

warn: Async::HTTP::Client: connected to #<Addrinfo: [::1]:3000 TCP (localhost)> 
      | Waiting for Async::HTTP::Protocol::HTTP1 pool to drain: #<Async::Pool::Controller(1/∞) 1/1/1>

Async error message

I really was tearing my hair out and briefly considered sending POSTs with the venerable HTTParty wrapped in an async block (I'm going to gush again about no coloured functions❤️)

This is all I needed to do: it's sets up a background reader to process the async HTTP response! doh!

def post(document, to:)
  Async do
    internet = Async::HTTP::Internet.new
    body = document.to_json
    endpoint = to
    Async do
      internet.post(endpoint, headers, body)
    end
  ensure
    internet.close
  end
end

Do this instead