servant-lint: Lint Servant Routes

[ bsd3, library, web ] [ Propose Tags ] [ Report a vulnerability ]

Lint Servant Routes and reject bad routes, overlaps, and more


[Skip to Readme]

Modules

[Index] [Quick Jump]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0.0, 0.1.1.0
Change log CHANGELOG.md
Dependencies base (>=4.18.2 && <5), bytestring (>=0.11.5 && <0.12), containers (>=0.6.7 && <0.7), safe-coloured-text (>=0.2.0 && <0.3), servant (>=0.20.1 && <0.21), servant-multipart (>=0.12 && <0.13), servant-server (>=0.20 && <0.21), text (>=2.0.2 && <2.1) [details]
License BSD-3-Clause
Author Isaac Shapira
Maintainer [email protected]
Category Web
Source repo head: git clone https://gitlab.com/platonic/servant-lint.git
Uploaded by fresheyeball at 2025-10-16T18:54:13Z
Distributions
Downloads 46 total (3 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for servant-lint-0.1.1.0

[back to package description]

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.