apipie-rails
.Strong parameters have their supporters and critics. Whether you use them or not, you still should remember about validating values. params.require(:user).permit(:first_name, :last_name)
is usually not enough to make Ruby on Rails application secure enough.
My reflections came from an issue that happened to me recently. I made a silly typo and it was enough to cause an avalanche of 500
errors which could have been easily avoided. A microservice I was working on made synchronous HTTP POST
requests to Ruby on Rails application. One of the required parameters was class name. After receiving a request it was converted to a concrete instance of a class:
params[:class_name].constantize.find(params[:id])
It had been working as expected until, by making a typo, I provided a class name which did not exist in the application:
"User".constantize.find(1) => #<User id: 1, email: "[email protected]", created_at: "2018-04-20 08:52:47", updated_at: "2018-04-20 08:52:47"> "Userr".constantize.find(1) NameError: uninitialized constant Userr
From the consumer point of view 422
(Unprocessable entity
) or 400
(Bad request
) response status code returned by the provider would:
- be more correct,
- prevent server error causing SMS alerts,
- reduce my and my colleagues’ stress level 🙂
There are multiple ways to make controller actions more bulletproof. I would like to share with you two, I am familiar with.
1. Manual validation
Let’s create a simple controller to use it as an example first. For the sake of simplicity, let’s assume that there is no ActiveRecord validation on model level. To be able to test actual behaviour I created a simple Rails 5.2 application in API-only mode (rails new rooms --api
).
class Api::RoomsController < ApplicationController def create Room.create!(params) end end
That is not the most beautiful controller in the world, for at least two reasons:
- We do not know which parameters are acceptable.
- We do not know which values are acceptable.
I tested it quickly by making a simple cURL request:
curl -H "Content-Type: application/json" -X POST -d '{"room": { "floor": 1 } }' localhost:3000/api/rooms
And got the following response:
{"status":500,"error":"Internal Server Error","exception":"ActiveModel::ForbiddenAttributesError...
ActiveModel::ForbiddenAttributesError
occurred. It is quite mysterious, though. Well, in that case, we cannot go forward without strong parameters 🙂
class Api::RoomsController < ApplicationController def create Room.create!(create_params) end private def create_params params.require(:room).permit(:owner_type, :owner_id, :floor, :price) end end
After the above changes, I retried the cURL request and got the below response:
{"id":3,"floor":1,"price":null,"owner_type":null,"owner_id":null,"created_at":"2019-03-05T19:32:36.077Z","updated_at":"2019-03-05T19:32:36.077Z"}
I managed to create a new entry, but with a lot of null
values which is undesired behaviour. Thanks to strong parameters we know which parameters the action accepts. Unfortunately, we still:
- know nothing about permitted values allowing API consumers to create valid records.
- have application vulnerable to code injection exploitation.
Let’s try to fix that without any external dependency:
class Api::RoomsController < ApplicationController class UnpermittedParameterValue < RuntimeError def initialize(parameter:, value:) @parameter = parameter @value = value end attr_reader :parameter, :value end rescue_from UnpermittedParameterValue, with: :invalid_parameters def create validate_create_params Room.create!(create_params) end private # Could be extracted to a dedicated class within e.g. Validation module. # Validation checks should support all the permitted parameters; below # only owner_type parameter is handled. def validate_create_params owner_type = create_params[:owner_type] raise UnpermittedParameterValue.new(parameter: :owner_type, value: owner_type) if !Set['Company', 'Person'].include? owner_type # and so on... end def invalid_parameters(exception) render json: { errors: { exception.parameter => "'Value #{ exception.value }' is not supported value for the parameter." } }, status: 400 end def create_params params.require(:room).permit(:owner_type, :owner_id, :floor, :price) end end
Thanks to the above changes, we know that either Company
or Person
can be an owner of Room
. Another step in the right direction. However, there are still some downsides:
- We need to define a dedicated validation method(s) or class(es) per action/parameter.
- We need to open and analyse source code to find the permitted values.
Re-running the cURL request with owner_type
parameter containing a string with a typo produced the below JSON response with 400
status code:
{"errors":{"owner_type":"Value 'Peeerson' is not supported value for the parameter."}}
Better, but still not ideal. Let’s check if adding some external dependencies would make the controller better.
2. apipie-rails
gem
The gem advertises itself as Ruby on Rails API documentation tool. In reality, it allows us to do more than documenting API. Thanks to its DSL, it makes validating API a cakewalk.
To make usage of it, you need to add it within application’s Gemfile
and invoke rails g apipie:install
afterwards. After doing so, we can apply its magic powers to the create
action:
class Api::RoomsController < ApplicationController api :POST, "/rooms", "An end-point used for creating new rooms in the system. Standard basic auth is required." formats ['json'] error 401, "Unauthorized" error :unprocessable_entity, "Could not create the room." param :room, Hash, desc: "Room details" do param :owner_type, ["Company", "Person"], desc: "Owner of the room", required: true param :owner_id, :number, desc: "ID of the room's owner", required: true # Below a simple ':number' validation is used. It could be extended to allow only supported floor e.g. from 1 to 12. param :floor, :number, desc: "Floor on which the room is located", required: true end def create Room.create!(create_params) end private def create_params params.require(:room).permit(:owner_type, :owner_id, :floor) end end
Voilà! We have just:
- Documented the
create
action. - Added quite exhaustive validation to it.
- (BONUS!) Get ready-to-use end-point documentation in HTML, JSON & Swagger formats for free.

Main page of API documentation generated by the gem. It presents a list of all described resources with defined descriptions.

A detailed page of API documentation generated by the gem. Thanks to it, API consumer can understand the endpoint better.
Any downsides? Yes, the gem returns 500
status response for errors like Apipie::ParamMissing
and Apipie::ParamInvalid
:
{"status":500,"error":"Internal Server Error","exception":"Apipie::ParamInvalid: Invalid parameter 'owner_type' value \"Peeerson\": Must be one of: 'Company', 'Person'."
If we would like to return 400
response instead, and I really would like to do so to eliminate 500
errors in case of typos, we would need to rescue the errors and handle them differently (for example in ApplicationController
to make all controllers behave the same through inheritance):
class ApplicationController rescue_from Apipie::ParamInvalid do |exception| render status: 400, json: { status: 400, param: exception.param, value: exception.value, error: exception.to_s, } end end
Thanks to the above change, we get the below response:
{"status":400,"param":"owner_type","value":"Peeerson","error":"Invalid parameter 'owner_type' value \"Peeerson\": Must be one of: 'Company', 'Person'."}
README.md
of the gem is an example of very good documentation. I encourage you to take a look if you want to discover all of its possibilities.
Why is it important?
I treat application’s HTTP API endpoints like its public interface. From a consumer point of view, they should be well-documented to allow smooth integration. As soon as I need to dip deep into the source code to understand what parameters and values are allowed, a small red bulb turns on in my head.
Solid API documentation combined with proper parameters validation:
- reduces the number of questions from consumers, making integration(s) on HTTP layer more pleasant,
- makes the application more secure,
- makes the application more reliable,
- makes our work easier 🙂
To fix the issue I encountered, I improved API validation which had already been defined using apipie-rails
DSL. A validation rule for the parameter value I made a typo in was set to String
. I spent some time analyzing source code and replaced it by a set of allowed values: ["name_of_first_supported_class", "name_of_second_supported_class"].
Thanks to that:
- similar mistakes will result in more proper
400
status code, - code injection is impossible.
I hope you learnt something today. There are ready-to-use tools that you can use to document and validate API end-points in Ruby on Rails application(s) you develop with minimum effort. It is worth the effort 🙂
P.S. If you handle the described issue differently, please share your thoughts in the comments.