Handling a custom response body with Faraday middleware

I've been working with a REST API recently that returns JSON except for one particular endpoint. This endpoint returns a plain text response with a non-standard content type header.

It looks like this:

1
2
3
4
5
6
A:1AHGJDSMMPLMPPGNLJBQVLBRSKVDLQRPP:2213,2214,2215
A:201AGBHDRLQHNHPHKKMPKLGPMDRDTDMVL:3134,3135,3136,3137
U:1AHGJDSMMPLMPPGNLJBQVLBRSKVDLQRPP:2212
U:201AGBHDRLQHNHPHKKMPKLGPMDRDTDMVL:3133
X:2AVDSSBSTSQDRDKBHCNTRTHPNTBGQDTMD:2677,2685,2969,2996,3002
X:1AHGJDSMMPLMPPGNLJBQVLBRSKVDLQRPP:3029,3056

The output is seat availability for concert venues with colons separating the fields like so <seat-type>:<band-id>:<seats>. The problem is that we need to handle this response explicitly as the default JSON parsing doesn't know what to do with it.

The desired output in this case would be like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
  {
    :seat_type => "A",
    :band_id => "1AHGJDSMMPLMPPGNLJBQVLBRSKVDLQRPP",
    :seats => ["2213", "2214", "2215"]
  },
  {
    :seat_type => "A",
    :band_id => "201AGBHDRLQHNHPHKKMPKLGPMDRDTDMVL",
    :seats => ["3134", "3135", "3136", "3137"]
  },

  ...
]

Faraday Middleware

I'm using Faraday as the HTTP client for this project and it has the concept of middleware. Middleware can apply to requests or responses and are hooked into the lifecycle of a request.

Faraday is an HTTP client library that provides a common interface over many adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle.

This means that we can write a custom middleware to parse the response for our custom content type, which for the purpose of illustration, we'll call venue/seats.

Custom response middleware

First we make sure we have the correct gems.

1
2
gem 'faraday', '~> 1.0'
gem 'faraday_middleware', github: 'lostisland/faraday_middleware', ref: 'e1324ca'

The latest version of faraday_middleware isn't compatible with Faraday 1.0 as far as I can tell at the moment, so I pinned the gem to commit e1324c, which is. There is a pre-release version available at the time of writing.

Now we can write a middleware that we will later hook into the request/response cycle. I'm using the ResponseMiddleware from faraday_middleware as it has a neat define_parser helper which allows us to very easily supply a parser for venue/seats content type.

1
2
3
4
5
6
7
8
9
10
11
12
13
require "faraday"
require "faraday_middleware"
require "faraday_middleware/response_middleware"

# Custom response middleware
class SeatAvailabilityResponse < ::FaradayMiddleware::ResponseMiddleware
  define_parser do |body, _|
    SeatAvailability.parse(body)
  end
end

# Register the middleware so we can use it
Faraday::Response.register_middleware(seat_availability: SeatAvailabilityResponse)

At the bottom you can see that I register this class as a response middleware called "seat_availability" using Faraday::Response.register_middleware. This allows us to refer to this middleware in the future.

Response parsing

I also delegate parsing responsibility to another class, SeatAvailability. You could just as easily add it inline, but I like to separate the responsibilities, and it also makes it easier to test if it's a separate class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SeatAvailability
  def self.parse(body)
    return if body.nil?

    body.split.map do |section|
      seat_type, band_id, seats = section.split(":")
      {
        seat_type: seat_type,
        band_id: band_id,
        seats: seats.split(",")
      }
    end
  end
end

Registering the middleware

And this is how we use the new middleware. We create Faraday connection object and in the block call conn.response in order to add it.

1
2
3
4
5
6
7
8
conn = Faraday.new("http://example.com/api") do |conn|
  #...

  # Use the `SeatAvailabilityResponse` middleware for "venue/seats" content types
  conn.response :seat_availability, content_type: "venue/seats"

  #...
end

Now when we make a call to an endpoint that returns a body with a venue/seats content type it will automatically be parsed into an array of hashes for us.

1
2
3
4
5
6
7
8
9
10
11
12
[1] (pry) main: 0> response = conn.get("availability")
=> [{
      :seat_type => "A",
      :band_id => "1AHGJDSMMPLMPPGNLJBQVLBRSKVDLQRPP",
      :seats => ["2213", "2214", "2215"]
    },
    {
      :seat_type => "A",
      :band_id => "201AGBHDRLQHNHPHKKMPKLGPMDRDTDMVL",
      :seats => ["3134", "3135", "3136", "3137"]
    }
   ]

I hope this demonstrates how easy it is to handle custom responses in a very straight-forward and modular way.