haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.81k stars 409 forks source link

Redirect combinator ? #117

Open alpmestan opened 9 years ago

alpmestan commented 9 years ago

Hi everyone,

This has been discussed a bit offline but I thought I should open an issue and throw some code in here for discussion and potential inclusion in servant. The deal here is to offer a proper generic combinator for redirection.

One thing however is that we have some decent machinery for generating type safe links, which of course looks like a good basis to build the redirect combinator on.

For a servant-0.4 compatible solution, one could consider such an implementation:

class KnownMethod (c :: [*] -> * -> *) where
  methodOf :: Proxy c -> Method

instance KnownMethod Delete where
  methodOf _ = methodDelete

instance KnownMethod Get where
  methodOf _ = methodGet

instance KnownMethod Patch where
  methodOf _ = methodPatch

instance KnownMethod Post where
  methodOf _ = methodPost

instance KnownMethod Put where
  methodOf _ = methodPut

data Redirect (c :: [*] -> * -> *) lnk api

instance (KnownMethod c, IsElem lnk api, HasLink lnk)
        => HasServer (Redirect c lnk api) where

  type ServerT (Redirect c lnk api) m
    = m URI

  route Proxy getLink req respond
    | null (pathInfo req) && requestMethod req == methodOf pc = do
        res <- runEitherT getLink
        case res of
          Left err  -> respond . succeedWith $ responseServantErr err
          Right lnk -> respond . succeedWith $
            responseLBS seeOther303 [("Location", fromString ("/" ++ show lnk))] "" 
    | null (pathInfo req) && requestMethod req /= methodOf pc =
        respond $ failWith WrongMethod 
    | otherwise = respond $ failWith NotFound

    where pc = Proxy :: Proxy c

(complete usable code here)

The only thing I'm not happy about is that I hardcode the 303 status code. We could probably take an additional parameter of kind Natural (type-level natural number) and only have the HasServer instance when that number is a valid status code used for redirects.

Thoughts?

PS: I use the above combinator in a work project already and hence would be keen to eventually include such a combinator in servant itself since I would otherwise have to maintain that combinator independently and adapt it for any change to servant/servant-server.

jkarni commented 9 years ago

I like this a lot. Here are some thoughts:

alpmestan commented 9 years ago
alpmestan commented 9 years ago

Had a little idea about this. If we don't want to be too opinionated about redirects, we can do:

data Redirect (status :: Natural) (c :: [*] -> * -> *) lnk api
    deriving (Typeable)

class KnownMethod (c :: [*] -> * -> *) where
  methodOf :: Proxy c -> Method

instance KnownMethod Delete where
  methodOf _ = methodDelete

instance KnownMethod Get where
  methodOf _ = methodGet

instance KnownMethod Patch where
  methodOf _ = methodPatch

instance KnownMethod Post where
  methodOf _ = methodPost

instance KnownMethod Put where
  methodOf _ = methodPut

instance (KnownMethod c, 300 <= st, st <= 307, IsElem lnk api, HasLink lnk)
        => HasServer (Redirect st c lnk api) where
jkarni commented 9 years ago

Nice. For type-safe conformance to the relevant RFCs, I think we'd really want something like

 (..., (st == 303 `And` IsGet lnk) `Or` (st == 307 `And` c ~ MethodOf lnk), ...)

&c. That way, we have a single instance, but all the nuances of the differences between code checked and built-in.

Of course, we would probably factor it out as a IsGoodRedirect type family so it doesn't look as ugly. @alpmestan what do you think?

alpmestan commented 9 years ago

Do you really want to enforce that the method of the endpoint we redirect from should be the same as the link's? That defeats the typical POST new entity -> redirect to its uri. But I'm totally onboard with your suggestion of having types enforce the RFC.

jkarni commented 9 years ago

307 does not allowing changing of method. 303 requires changing to GET. See https://tools.ietf.org/html/rfc7538

jkarni commented 9 years ago

Any updates on this? I have a little time and could do it.

alpmestan commented 9 years ago

Oh, if you want to, please be my guest! I'd definitely be glad to review and then actually use the final Redirect combinator as described in the last few comments here :)

jkarni commented 9 years ago

Ok, @alpmestan and I discussed this a little more. The current proposal doesn't tie the link you actually use with the link in the type at all, which is bad. One option is to get the type for safeLink from the combinator, which would stay much as it is. The consequences are that

  1. The link would have to be static
  2. We'd have to pass the handler a continuation so that it could provide any missing params

The alternative is to change the Link type so that we can tag it with the API type it was constructed for, and then just require one type param for the Redirect combinator (the API type), and check that that and the Link type match. The second involves more changes, but seems more general. Any thoughts?

jkarni commented 9 years ago

Actually, we should be tagging links anyhow.

alpmestan commented 9 years ago

I'd be keen to see a PR with the first approach first, and then see if we really need the tagging. I'm not sure of what would be tagged anyway since links just end up being URIs. However, if you really want to go with the alternative approach, I won't argue against it.

WongKamFu commented 8 years ago

How do I redirect to a static file? I'm trying to redirect to a HTML file after successfully doing an insert (CRUD). Tried responseFile but doesn't typecheck because it returns Response. Does Servant got something like Scotty's redirect?

alpmestan commented 8 years ago

You can do it "the good old way" I guess:

left $ errXXX { errHeaders = [("Location", "/foo.html")] }

where XXX is one of the redirect status codes (301, ...). This should work in any handler (and is a bit of a hack/escape hatch, yes).

WongKamFu commented 8 years ago

@alpmestan thanks for the pointer. CMIIW, servant's using Except instead of Either, right?

alpmestan commented 8 years ago

Yeah, in the master branch of this repo (which is the upcoming 0.5 release's branch, basically). But it's not on hackage yet.

adnelson commented 7 years ago

Has there been any more movement on this? I'm surprised that it seems to have died out seeing as (a) redirects are pretty common in http programming and (b) it looks like code for this (or at least a prototype) was already done when this post was made.

I don't understand the internals of Servant well enough to implement it myself, but if there's something usable out there (even if not bullet-proof) then I'd be interested in trying it out. Otherwise I guess using ServantErr is the only way to do it?

alpmestan commented 7 years ago

ServantErr is the "escape hatch" way to do redirects. The goal of this issue was to discuss some smarter redirection mechanism, one which would be able to check for example that the uri it redirects to belongs to some API type. I wrote the code above for a consulting gig back then and it was working fine. It is however out of data.

I agree however that this would be a very useful addition. It would even be about time that we have a proper solution for redirects. @dmjio very recently expressed interest in that too. If we get people to agree on what this redirect combinator should support, what it should look like, I can definitely provide some guidance for anyone interested in implementing it.

erlandsona commented 6 years ago

Any news on the progress of this? I just ran into a situation with a small elm app I built where I want to use a redirect to keep my root route / instead of index.html, which yes I could do in any number of ways but I landed here and this struck me as interesting.

alpmestan commented 6 years ago

Nothing in particular, although there's been a bit more discussion in https://github.com/haskell-servant/servant/issues/135. Relatedly, @phadej and I have been discussing an overhaul of some existing combinators that would among many things give a principled story about redirects, you can read more about this at https://github.com/haskell-servant/servant/issues/841

domenkozar commented 6 years ago

Maybe we could define Get302/Post302 Verb that would require Headers '[Header "Location" String] in response type?

alpmestan commented 6 years ago

We definitely can define a "shortcut": type Get302 (cts :: [*]) a = Verb 'GET 302 cts (Headers '[Header "Location" String] a). And maybe we could define an alias for 'addHeader' specialized to producing a suitable Headers ... a value. Not sure what people would like?

domenkozar commented 6 years ago

@alpmestan I can't figure out how would nesting Headers work then:

      Get302 '[PlainText] (Headers '[ Header "Set-Cookie" SetCookie
                                    , Header "Set-Cookie" SetCookie
                                    ] NoContent)
alpmestan commented 6 years ago

Oh, if you have other headers you'd probably instead do:

type Get302 (cts :: [*]) (hs :: [*]) a = Verb 'GET 302 cts (Headers (Header "Location" String ': hs) a)

where hs would be the list of extra headers, in addition to "Location". Of course, hs would be '[] if no other response header needs to be set.

domenkozar commented 6 years ago

That works, thanks!

EDIT: server part doesn't compile, let me check.

alpmestan commented 6 years ago

There's nothing magic going on above. You just set in stone the things you want (HTTP method, status code, "Location" header) and leave everything else as parameters to your custom Get. I'm suspecting the type errors you'll see have to do with using addHeader enough times, and in the right order maybe?

domenkozar commented 6 years ago

Yes - I didn't know GHC doesn't fully resolve type synonyms in error messages, so I was misguided.

I'd say having Get/Post 302/301 would be a good step forward towards having redirection and more type safety can be added later on. Would you accept such PR if I give it a try?

alpmestan commented 6 years ago

Absolutely (speaking in my name only of course) ! It would be perfect if you could even perhaps mention them in the server part of the tutorial or a small dedicated cookbook recipe? So that people can more easily discover them and how they should be used.

runeksvendsen commented 3 years ago

I'd love to be able to do the following in a type-safe manner using Servant:

Consider a simplified "blog" API with endpoints:

  1. GET 302: /post/:post_id/comments/newest
  2. GET 200: /post/:post_id/comments/:comment_id

The first endpoint asks the API server "what is the comment_id of the newest comment for the post with the given post_id?". The server responds with a HTTP 302 response indicating the URL of this comment in the Location header -- essentially "filling out" the :comment_id parameter in the second endpoint URL.

This is useful in scenarios where the following conditions apply:

If these requirements are fulfilled, using this redirect schema means a caching middleware can cache responses to /post/:post_id/comments/:comment_id with an infinite "expiration", and cheaply respond to /post/:id/comments/newest with an URL of a (possibly already-cached) comment

Is that what this proposal is about? Specifically, the above requires a combinator which inherits the return type (e.g. Comment) of another API endpoint, so that the API endpoint which redirects uses the return type of the endpoint to which it redirects.