Skip to content

servant-auth-server: support MultiVerb endpoints behind Auth - #1922

Open
nuttycom wants to merge 1 commit into
haskell-servant:masterfrom
nuttycom:feat/auth-multiverb
Open

nuttycom wants to merge 1 commit into
haskell-servant:masterfrom
nuttycom:feat/auth-multiverb

Conversation

@nuttycom

@nuttycom nuttycom commented Oct 13, 2026 •

Copy link
Copy Markdown

An API with a MultiVerb endpoint under Auth does not compile today: Auth adds its Set-Cookie headers to every endpoint below it through AddSetCookieApi and AddSetCookies, and neither has an instance for MultiVerb.

This adds them.

  • Each response of a MultiVerb endpoint gets an optional Set-Cookie header: WithHeaders '[OptHeader (Header "Set-Cookie" SetCookie)] (ResponseType a, Maybe SetCookie) a.
  • The handler is written as before; Auth pairs its result with the cookie (AddSetCookies), one layer per cookie, like the Verb case.
  • The result type of the cookied endpoint depends on its number of responses:
    • one response: (r, Maybe SetCookie). servant's own AsUnion '[r] a instance converts it, which is also why a wrapper type cannot be used here: an AsUnion bs (Wrapper r) instance would overlap with AsUnion '[r] a.
    • several responses: WithSetCookie as r, whose AsUnion instance converts r with AsUnion as r and moves the cookie into whichever response the handler gives (AddSetCookieUnion).

sop-core becomes a direct dependency of servant-auth-server, for NS and I; servant already depends on it.

Tests

Servant.Auth.ServerSpec gains routes behind Auth, with examples that check the cookie names Auth sets (session and XSRF, so every endpoint stacks two Set-Cookie layers):

  • a two-response MultiVerb (RespondEmpty 204 / Respond 200 Int, handler result Maybe Int), on both of its responses;
  • a MultiVerb1;
  • a MultiVerb1 whose response has its own Blah header, which is kept;
  • a two-response MultiVerb whose 200 response sets its own Set-Cookie, which is kept with its value;
  • a MultiVerb whose handler answers unauthenticated requests itself: the 401 response keeps its own header and has no Set-Cookie header, and the authenticated 200 response has both cookies.

cabal test servant-auth-server:spec with GHC 9.12: 39 examples, 0 failures, 2 pending (pending before this change).

Filed with Claude Code assistance.

@nuttycom
nuttycom force-pushed the feat/auth-multiverb branch from 00553c2 to b2cfaa7 Compare October 13, 2026 14:40
nuttycom added a commit to nuttycom/mcp that referenced this pull request Oct 13, 2026
The Streamable HTTP transport requires a server to answer a POST that
carries only a notification or a response with 202 Accepted and no body.
The POST route answered every message with a 200 event stream: a
notification got an invented `{"method":"ok"}` notification, and a
response got an empty stream. Clients that follow the specification,
such as Codex, close the connection after `notifications/initialized`.

The POST route of `MCPAPI` and `SimpleHTTPAPI` is now a `MultiVerb` over
`MCPPostResponses`: 202 with no body for a notification or a response,
and 200 with the JSON-RPC messages as Server-Sent Events for a request.
A response to a request the server never sent gets 400, instead of an
error inside a 200 stream.

servant-auth-server has no instances for `MultiVerb`, so the internal
module `MCP.Server.HTTP.AuthMultiVerb` gives them until a release
includes them
(haskell-servant/servant#1922). The `servant-auth-server < 0.4.11`
bound keeps the two from clashing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +582 to +584
:<|> "multiverb" :> MultiVerb 'GET '[JSON] MaybeLength (Maybe Int)
:<|> "multiverb" :> "empty" :> MultiVerb 'GET '[JSON] MaybeLength (Maybe Int)
:<|> "multiverb1" :> MultiVerb1 'GET '[JSON] (Respond 200 "The length of the name" Int)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we see a test case with the use of other headers? I want to make sure that the addition in the PR composes correctly with either other random headers, or even with another SetCookie

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 1c0095a. Every MultiVerb test now checks the full set of cookie names. Auth always sets two cookies (session and XSRF), so each endpoint stacks two Set-Cookie layers. New cases:

  • /multiverb/header: a MultiVerb1 whose response has its own Blah header. The header comes back next to both Auth cookies.
  • /multiverb/cookie: a two-response MultiVerb whose 200 response sets its own Set-Cookie. The handler's cookie arrives with its value, next to both Auth cookies.
  • /multiverb-auth: a MultiVerb behind Auth whose handler answers unauthenticated requests itself with a 401 that has a Blah header. That response keeps Blah and has no Set-Cookie header (the case where both cookies are Nothing). An authenticated request gets the 200 with both cookies.

Reply drafted with Claude Code assistance.

An API with a `MultiVerb` endpoint under `Auth` did not compile:
`AddSetCookieApi` and `AddSetCookies` had no instances for `MultiVerb`.

Each response of a `MultiVerb` endpoint now gets an optional `Set-Cookie`
header. The handler is written as before; `Auth` pairs its result with
the cookie. An endpoint with one response takes the pair directly,
because servant's own `AsUnion '[r] a` instance converts it. An endpoint
with several responses takes `WithSetCookie as r`, whose `AsUnion`
instance moves the cookie into whichever response the handler gives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nuttycom
nuttycom force-pushed the feat/auth-multiverb branch from b2cfaa7 to 1c0095a Compare October 16, 2026 05:36
@nuttycom
nuttycom requested a review from LaurentRDC October 16, 2026 16:05

@LaurentRDC LaurentRDC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very close, just one more check for completeness

opts <- addJwtToHeader jwt
resp <- getWith opts (url port ++ "/multiverb-auth")
resp ^. responseStatus `shouldBe` status200
resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a check that response header "Blah" is missing?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants