Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirect combinator ? #117

Open
alpmestan opened this issue Jun 10, 2015 · 28 comments
Open

Redirect combinator ? #117

alpmestan opened this issue Jun 10, 2015 · 28 comments

Comments

@alpmestan
Copy link
Contributor

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
Copy link
Member

jkarni commented Jun 10, 2015

I like this a lot. Here are some thoughts:

  • Why not name it Redirect303 or SeeOther, or something along those lines? We can add other redirects later.
  • If we can abstract away commonality of redirects, and then wrap more specific combinators into newtypes, great. I don't think we should make that a blocker, though.
  • The 303 redirect should check that lnk is a GET endpoint (cf. RFC 7231)

@alpmestan
Copy link
Contributor Author

  • Why not, actually. This idea hasn't even crossed my mind for some reason =) It's not like there would be hundreds of them, we were talking about absolutely having to support 4 or 5 status codes the other day, right?
  • We could however have these be type synonyms for an application of a generic Redirect combinator that would take a status code parameter, and have some KnownStatusCode class do the discrimination on the status code to allow the HasServer instance to be picked.
  • Oh, I wasn't aware of that, although I have not violated this principle. Instinct, I guess. Should we do that with a simple type family IsGetLink :: * -> Bool and have a IsGetLink lnk ~ 'True constraint on the instance?

@alpmestan
Copy link
Contributor Author

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
Copy link
Member

jkarni commented Jun 18, 2015

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
Copy link
Contributor Author

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
Copy link
Member

jkarni commented Jun 18, 2015

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

@jkarni
Copy link
Member

jkarni commented Jul 9, 2015

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

@alpmestan
Copy link
Contributor Author

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
Copy link
Member

jkarni commented Jul 9, 2015

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
Copy link
Member

jkarni commented Jul 9, 2015

Actually, we should be tagging links anyhow.

@alpmestan
Copy link
Contributor Author

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
Copy link

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
Copy link
Contributor Author

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
Copy link

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

@alpmestan
Copy link
Contributor Author

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
Copy link

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
Copy link
Contributor Author

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
Copy link

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
Copy link
Contributor Author

Nothing in particular, although there's been a bit more discussion in #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 #841

@domenkozar
Copy link
Contributor

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

@alpmestan
Copy link
Contributor Author

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
Copy link
Contributor

domenkozar commented Apr 15, 2018

@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
Copy link
Contributor Author

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
Copy link
Contributor

domenkozar commented Apr 15, 2018

That works, thanks!

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

@alpmestan
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor Author

alpmestan commented Apr 16, 2018

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
Copy link

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:

  • Performing /post/:id/comments/newest is relatively cheap
  • Performing /post/:post_id/comments/:comment_id is relatively expensive
  • Comments are immutable (they cannot be edited)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants