Servant Lint
If you have worked with APIs, you have likely encountered ambiguous routes. These issues are painful to debug and can present as "reality violations". Questions like "Why isn't my handler being called?" and "What is happening with type marshaling?" often arise. Additionally, some APIs may promise a JSON parsable output while sending zero bytes, leading to further confusion. This project aims to detect these kinds of problems in Servant API types.
Rejections
- Straighforward ambiguous overlaps
- Ambiguous overlaps of Capture and CaptureAll with static
- Ambiguous overlaps of Capture and CaptureAll
- Ambiguous overlaps due to QueryParams
- No ReqBody with GET requests
- NoContent must be 204
- ReqBody not adjacent to Verb
- Duplicate combinator hints
- 500 as a success response
- Routes accepting the same type multiple times (argument order ambiguity)
- Multiple QueryParam with the same name
Output Sample
type API =
= "bar" :> CaptureAll "murf" [Int] :> Get '[JSON] ()
:<|> "bar" :> "5" :> Get '[JSON] ()
:<|> "bar" :> Capture "how" Int :> Get '[JSON] ()
:<|> "bar" :> "3" :> "2" :> Get '[JSON] ()
main :: IO ()
main = lintAPI @API
Produces:
NOTE: Output will NOT exactly match your type definition
Ambiguous with "bar" :> CaptureAll "murf" [Int] :> Verb 'GET 200 ():
"bar" :> CaptureAll "murf" [Int] :> Verb 'GET 200 () 👈
"bar" :> "5" :> Verb 'GET 200 () 👈
"bar" :> Capture "how" Int :> Verb 'GET 200 () 👈
"bar" :> "3" :> "2" :> Verb 'GET 200 () 👈
Ambiguous with "bar" :> "5" :> Verb 'GET 200 ():
"bar" :> CaptureAll "murf" [Int] :> Verb 'GET 200 () 👈
"bar" :> "5" :> Verb 'GET 200 () 👈
"bar" :> Capture "how" Int :> Verb 'GET 200 () 👈
"bar" :> "3" :> "2" :> Verb 'GET 200 ()
Ambiguous with "bar" :> Capture "how" Int :> Verb 'GET 200 ():
"bar" :> CaptureAll "murf" [Int] :> Verb 'GET 200 () 👈
"bar" :> "5" :> Verb 'GET 200 () 👈
"bar" :> Capture "how" Int :> Verb 'GET 200 () 👈
"bar" :> "3" :> "2" :> Verb 'GET 200 ()
Ambiguous with "bar" :> "3" :> "2" :> Verb 'GET 200 ():
"bar" :> CaptureAll "murf" [Int] :> Verb 'GET 200 () 👈
"bar" :> "5" :> Verb 'GET 200 ()
"bar" :> Capture "how" Int :> Verb 'GET 200 ()
"bar" :> "3" :> "2" :> Verb 'GET 200 () 👈
But with fancy colors:
Duplicate Type Detection
Routes that accept the same type in multiple places can cause argument order confusion:
type BadAPI =
"user" :> Capture "id" Int :> ReqBody '[JSON] Int :> Post '[JSON] ()
:<|> "search" :> Capture "userId" String :> QueryParam "name" String :> Get '[JSON] ()
main :: IO ()
main = lintAPI @BadAPI
Produces:
Route accepts the same type multiple times: Int. This doesn't guarantee argument order and can lead to ambiguous behavior:
"user" :> Capture "id" Int 👈 :> ReqBody _ _ Int 👈 :> Verb 'POST 200 ()
Route accepts the same type multiple times: [Char]. This doesn't guarantee argument order and can lead to ambiguous behavior:
"search" :> Capture "userId" [Char] 👈 :> QueryParam "name" [Char] 👈 :> Verb 'GET 200 ()
Duplicate QueryParam Names
Routes cannot have multiple QueryParam with the same name:
type BadQueryAPI =
"search" :> QueryParam "filter" Int :> QueryParam "filter" Bool :> Get '[JSON] ()
main :: IO ()
main = lintAPI @BadQueryAPI
Produces:
Route has multiple QueryParam with the same name: filter. QueryParam names must be unique within a route:
"search" :> QueryParam "filter" Int 👈 :> QueryParam "filter" Bool 👈 :> Verb 'GET 200 ()
Usage
Servant Lint is best used in your test suite.