Introducing We::Call, a Ruby Gem for making HTTP calls awesome

HTTP calls are simultaneously the easiest thing, and a labyrinth of potential unimaginable, terrible outcomes. Service-oriented Architectures (micro or otherwise) increase this issue drastically.

Setting out to solve as many of these issues as possible, We::Call was developed here at WeWork. A wrapper around Faraday, it pairs client middleware (faraday_middleware) and server middleware (Rack middleware) to provide powerful functionality that solve all sorts of issues.

Murder Mystery in Production

An issue arose where Service A bust a large cache of data from Service B. This started a stampeding herd against Service B, which died in production.

Service B had more than a few dependents, so finding the murderer was more than a little complex. The endpoint was not secured by any API key or token, and as such the logs could not give us any sort of hints.

Eventually we noticed that User Agent: Faraday v0.10.1 was actually an accidentally unique identifier, as all other dependents were using v0.10.0 or v0.11.0. This was a huge fluke, and even then it was not exactly a speedy discovery.

To avoid this, We::Call::Connect will raise an exception if no app name is provided through config on usage. It falls back to ENV['APP_NAME'], or the name of the Rails application, if they’re there.

# Provided at config
connection = We::Call.configure do |config|
config.app_name = 'Service A'
end

# Provided at initialization
connection = We::Call::Connection.new(host: 'https://service-b.example.com/', app: 'Service A', timeout: 5)

This will set the User-Agent header on the request message, and X-App-Name which is handy to pass on just in case some network component munges the User-Agent.

Entropic Cascade Failure

As we learned from Stargate SG-1 S3E06 “Point of View”, when alternate realities cross over there can be dangerous outcomes.

We had a really nasty issue where somehow, through extremely unlikely circumstances, a staging application was hitting a production environment, and causing it to have a really bad time.

To avoid that, We::Call demands the environment be set (or detectable from ENV['RACK_ENV'] || ENV['RAILS_ENV']) before allowing a call to be made.

# Provided at config
connection = We::Call.configure do |config|
config.app_env = 'staging'
end

# Provided at initialization
connection = We::Call::Connection.new(host: 'https://service-b.example.com/', env: 'staging', timeout: 5)

But what I’m really excited about so far, is…

Deprecations

Hailed as tricky, difficult or nearly impossible to manage, deprecating endpoints has certainly not been a topic close to anyone’s heart in the API world.

Luckily, with progress being made on the Sunset header, this has got a little easier. We::Call leverages a gem called ruby_decorators to add Python-style decorators.

class FooController < ApiController
+We::Call::Deprecated.new(date: '2018-01-07 00:00:00 EDT')
def show
# ...
end
end

The + seems a little funky, but this is done to keep your actual method untouched. Regardless of how your method renders, this will jam the Sunset header into place all the same.

Thanks to some Faraday response middleware, and request made through We::Call will notice a Sunset header, and use ActiveSupport::Deprecation or ENV['rack.logger'] depending on what is available.

A We::Call-enabled client, noticing the Sunset header a We::Call-enabled Server populated.

I plan to release the detection middleware as a standalone gem “faraday-sunset”, as this useful for everyone, not just We::Call users.

Future Plans

The future for deprecations are powerful, and instead of just deprecating entire endpoints we plan to support detecting JSON Schema and OpenAPI definitions, the latter of which supports deprecations on fields, and the former may in an upcoming draft.

Beyond deprecations, pairing client-side and server-side middleware is incredibly powerful, and utilizing existing standards means that those not using We::Call can get involved too.

Come and pitch in, and make issues or pull requests with ideas and suggestions. I know this is an early version and the Rails dependency is really upsetting, so let’s make it better!