-
Notifications
You must be signed in to change notification settings - Fork 12
Specifying exceptions with Servant and Swagger
Be able to exert fine-grained control over errors defined in our Swagger API specification.
Our API is defined in Haskell with Servant, and translated into Swagger format using the servant-swagger
library.
Swagger makes it possible to specify that an endpoint can return one or more errors. For example, the following endpoint can return a 404
(not found) error:
"responses" :
{ "200" : { "schema" : {"$ref" : "#/definitions/Location" }
, "description" : "the matching location" } }
, "404" : { "description" : "a matching location was not found" }
By default, Servant doesn't provide a way for API authors to specify the errors that might be returned by individual endpoints.
In light of this, servant-swagger
takes the approach of auto-generating errors when various Servant combinators appear in the API. For example, when a Capture
combinator is used, servant-swagger
automatically inserts a 404
(not found) error in the generated Swagger output.
However, auto-generation of error responses has two problems:
- The generated error responses are sometimes incomplete.
- The generated error responses are sometimes inappropriate.
Consider the following endpoint, allowing the caller to add a new Location
to a location database:
type AddLocation = "location"
:> "add"
:> Summary "Add a new location"
:> Capture "locationName" Text
:> Put '[JSON] Location
By default, the generated Swagger output includes a 404 error (not found):
"/location/add/{locationName}" :
{ "put" :
{ "parameters" : [ { "in" : "path"
, "type" : "string"
, "name" : "locationName"
, "required" : true } ]
, "responses" :
{ "200" : { "schema" : { "$ref" : "#/definitions/Location" }
, "description" : "the added location" }
, "404" : { "description" : "`locationName` not found" } }
, "summary" : "Add a new location"
, "produces" : ["application/json;charset=utf-8"] } }
In the above example:
- The generated errors are inappropriate. Since we're adding a new location (and not looking up an existing location), this endpoint will never actually return
404
. - The generated errors are incomplete. We'd like to perform various validation checks on the new location name, and possibly return a
400
error if validation fails. However, this isn't indicated by the generated Swagger output.
Suppose that adding a Location
can fail in two ways, either because:
- the location name is too short; or
- the location name contains invalid characters.
We'd ideally like for the "responses"
section to reflect this:
"responses" :
{ "200" : { "schema" : { "$ref" : "#/definitions/Location" }
, "description" : "the added location" }
, "400" : { "description" :
"the location name was too short
OR
the location name contained invalid characters" } }
The servant-checked-exceptions
package defines the Throws
combinator, making it possible to specify individual exceptions as part of the endpoint definition:
type AddLocation = "location"
:> "add"
:> Summary "Add a new location"
:> Throws LocationNameHasInvalidCharsError
:> Throws LocationNameTooShortError
:> Capture "locationName" Text
:> Put '[JSON] Location
data LocationNameTooShortError = LocationNameTooShortError
deriving (Eq, Generic, Read, Show)
data LocationNameHasInvalidCharsError = LocationNameHasInvalidCharsError
deriving (Eq, Generic, Read, Show)
It's possible to assign specific response codes to individual exceptions. In our example, both exceptions will share the same response code 400
(bad request):
instance ErrStatus LocationNameHasInvalidCharsError where
toErrStatus _ = toEnum 400
instance ErrStatus LocationNameTooShortError where
toErrStatus _ = toEnum 400
For client code that's written in Haskell, the servant-checked-exceptions
library provides the very useful catchesEnvelope
function, allowing the caller to perform exception case analysis on values returned by an API.
So far so good. However there are two problems that we need to solve:
1. `servant-swagger` doesn't know what to do with the `Throws` combinator.
2. `servant-swagger` inserts its own default error response codes.