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 "seatavailability" using `Faraday::Response.registermiddleware`. 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.