Skip to content
This repository was archived by the owner on Mar 1, 2019. It is now read-only.

Specifying exceptions with Servant and Swagger

Jonathan Knowles edited this page Feb 21, 2019 · 27 revisions

Motivation

Be able to exert fine-grained control over errors defined in our Swagger API specification.

Background

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:

  1. The generated error responses are sometimes incomplete.
  2. The generated error responses are sometimes inappropriate.

Example

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:

  1. 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.
  2. 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.

What we really want

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" } }

How can we achieve this?

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.



Clone this wiki locally