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
OPTIONS support #278
Comments
Thanks a lot for this! What users actually are exposed to seems like the right direction to me. Some notes:
we have to make sure this would not cause the
Where This will still run into the same issue with headers preceding the Thoughts? |
I think I should take a look at that Verb idea after I finish the second bullet point above. :) The honest to goodness goal here really is CORS less than HATEOAS, but I hadn't yet moved to really understanding what CORS will take. |
CORS is pretty messy... One issue with the Verb design, I now realize, is that it doesn't work if the browser doesn't send an Access-Control-Request-Method, since the leaf can't look around itself to siblings easily. I don't know if that's fatal to the idea, since I don't really know how all of the browsers behave. For simplicity, I think it'd also be okay to limit configuration to Origin at first (and if course method, though that might work differently).
As in, HATEOAS is the real goal? It might be an easier one, since there we don't need any extra data to figure out what to allow besides what's already in the API. We do need to make sure the Origin is what we expect though, so that we don't accidentally allow everything on CORS. |
Oh, as in I'm not personally motivated to have My understanding is that what you'd want in an API spec is a description of what verbs and endpoints will support CORs, what headers and verbs are acceptable, and then have some exterior notion (maybe!) of allowable origins. A lot of the time the origin filter will just be |
So I have a partial expanded design which includes room for some CORS. It's kind of nice but I'm a little stuck on the type level stuff in one part. In particular, it is difficult to recurse on API types due to the fact that symbols are "bare" instead of being something like Anyway, here's the code -- | A "fully-loaded" Options method specifier.
--
-- Associated information describes the complete CORS policy for a given
-- endpoint, and, importantly, nothing below it.
data
Options
(allowedOrigins :: OriginSpec *)
(allowedHeaders :: [*])
(allowedMethods :: [*])
(exposedHeaders :: [*])
deriving Typeable
-- | When specifying allowed origins, the wildcard supercedes all other origin specification
-- and prohibits allowing credentialed requests. If not a wildcard then we
-- specify whether we allow credentials and a list of origin specifiers.
data OriginSpec a
= SameOrigin -- ^ Disable CORS for this resource
| Wildcard -- ^ Enable CORS for all origins
| AllowCredentials Bool [a] -- ^ Enable CORS for some origins, possibly with credentials
-- | A wrapper letting us access HTTP methods as direct data types.
-- Used in specifying the Options "allowedMethods" parameter
data Method (a :: [*] -> * -> *) :: *
-- | Analyzes a "local" endpoint gathering method information furnished.
type family GatherMethods api :: [*]
type instance GatherMethods (a :<|> b) = GatherMethods a ++ GatherMethods b
type instance GatherMethods (a :> b) = '[]
type instance GatherMethods (Get a b) = '[ Method Get ]
type instance GatherMethods (Put a b) = '[ Method Put ]
type instance GatherMethods (Post a b) = '[ Method Post ]
type instance GatherMethods (Patch a b) = '[ Method Patch ]
type instance GatherMethods (Delete a b) = '[ Method Delete ]
-- | Analyzes a "local" endpoint gathering response header information furnished.
type family GatherHeaders api :: [*]
type instance GatherHeaders (a :<|> b) = GatherHeaders a ++ GatherHeaders b
type instance GatherHeaders (a :> b) = '[]
type instance GatherHeaders (Get a (Headers hs b)) = hs
type instance GatherHeaders (Put a (Headers hs b)) = hs
type instance GatherHeaders (Post a (Headers hs b)) = hs
type instance GatherHeaders (Patch a (Headers hs b)) = hs
type instance GatherHeaders (Delete a (Headers hs b)) = hs
-- Compute Options API endpoint from the passed API endpoint "locally"
type AutoOptions cors headers api
= Options
cors
headers
(GatherMethods api)
(GatherHeaders api)
-- Extend an API locally with automatically computed options data
type WithOptions cors headers api
= AutoOptions cors headers api :<|> api
type family Finalize (gather :: [*]) api
type instance Finalize gather (Options cors reqHeaders reqMethods respHeaders) =
Options cors (reqHeaders ++ gather) reqMethods respHeaders
type instance Finalize gather (a :<|> b) =
Finalize gather a :<|> Finalize gather b
type instance Finalize gather (a :> b) =
a :> Finalize (CaptureHeaders a ++ gather) b
type family CaptureHeaders a :: [*]
type instance CaptureHeaders (Header name ty) = '[Header name ty] The final bit, Unfortunately, it needs a companion, |
This is mostly a POC again, btw. To really specify this correctly we need to differentiate between analysis which should be totally automated and CORS which is up to the API designer. For instance, it might be the case that one wishes to disable cross origin work on a given endpoint only partially. Ultimately, I think there needs to be some other constructor to indicate CORS data. If a design for Finalize could work that kind of analysis could be used. |
The other trick is that CORS data should be non-specific to Options headers but instead to endpoints in the tree, which don't really have direct representation in Servant. Options just reflects the nature of the endpoint. |
So perhaps that's the way to go. Segment "user" :>
Endpoint
'[ Cors Wildcard (Method Get '[JSON] User)
, Cors (WithCredentials 'True '[OriginSetA]) (Method Post '[JSON] User)
, Method Put '[JSON] UserUpdate
, Method Delete ()
] |
Can you explain this? Do you mean by endpoints in the tree not having a direct representation that we can't be sure we captured the entirety of the endpoint by a type family unless we know the type family was applied at the root of the API? |
Yeah, precisely. You could perhaps do a bottom up analysis to grab headers and methods that dumps all of it's information whenever it passes a url-changing element of the API tree, but otherwise you won't be able to do pre-flighting at an endpoint since it'll be hard to say if you're capturing all of the right methods. I suppose it's possible by searching through the |
Also, for interest I've been experimenting with these ideas at https://github.com/tel/serv. It has an Endpoint type like above (as well as more regular API types that are a bit easier to pass type functions over). |
I'm having this problem. Issuing POST requests with Swagger-ui (button "Try it out!") fails with:
There is no workaround at the moment, right? |
@cdupont I know I'm late, but see haskell-servant/servant-swagger#45 (comment) (in a nutshell, you just need |
I did that for one of my project and it works just fine. |
simpleCors doesn't work for me, on simple get requests it's fine, however when i POST or PUT to a resource, servant still does a 400 on the OPTIONS |
I have a full test project you can see the issue https://github.com/leshow/elm-tut/tree/master/app try updating a player level |
For those of you who are blocked on the options functionality I've made what I think is a reasonable workaround. I've created a WAI middleware that reflects on your servant app api to return options for requests: https://github.com/sordina/freewill.it/blob/20ad7348e0841a757abb2a8bbce6a39f6cf21ad3/src/Network/Wai/Middleware/Servant/Options.hs#L10#L10 This can then be used in conjunction with the wai-cors middleware like so: https://github.com/sordina/freewill.it/blob/20ad7348e0841a757abb2a8bbce6a39f6cf21ad3/src/Enhancements.hs#L31 If there's interest I might turn the options module into a package on Hackage. |
@sordina Looks very simple to use, I like that. I definitely think it can't hurt to upload this to hackage, as it will allow us to point people to it. OTOH I've not needed any of this in a long while, so I hope other people will jump in and give some feedback. |
@sordina I'll take a look at it this later today, thank you |
@haskell-servant/maintainers Can we consider this issue closed and delegate everything OPTIONS related to @sordina? =) |
@alpmestan I do feel like OPTIONS responses are fairly standard REST-server behaviour. It should probably be folded into servant-server if this is going to be the recommended solution. |
@sordina if someone were to use |
@fizruk Nope, if you want to do CORS you probably want to use both like so:
|
@sordina I was going to give this a shot but your middleware link is returning not found. Edit: sorry forgot to refresh the thread |
@leshow Ah sorry, I've created a seperate library on Hackage now, so I deleted the original source from the project that it was developed in. I'll hard code a commit into the links. Try again! |
@sordina ok, I must be missing something, but it seems to me that |
@sordina This is the error I get when I try to replicate your example
I've got app :: IO Application
app = do
s <- getServer
return
$ logStdoutDev
$ corsWithContentType
$ provideOptions api
$ serve api s
where
corsWithContentType :: Middleware
corsWithContentType = cors (const $ Just policy)
where
policy = simpleCorsResourcePolicy
{ corsRequestHeaders = ["Content-Type"] } Am I missing something? Adding servant-foreign as a dependency didn't seem to fix the issue either. |
Interesting, I just had another look at wai-cors, and it seems that you're right:
But I when I was performing testing it seemed to require both wai-cors, and servant-options... Hmm, well if it works just with wai-cors, then go with that I guess! Maybe there's still some other utility to servant-options when not using wai-cors though! |
@leshow I had similar errors when using options on APIs leveraging servant-swagger, servant-js, etc. I found that when I stuck to the core API things went smoothly though. I believe this is due to some extra instances needing definition. |
@sordina I'm not sure I follow. Can you give me a hint as to what I'm missing? https://github.com/leshow/elm-players/blob/master/app/server/src/Lib.hs |
@leshow You'd have to swap the order of cors and options, since options won't accept cross origin on its own, it just deals with options. |
Is this dead or still legitimately open? |
There are working solutions AFAICT, so we can perhaps close this? Any objection? |
Agreed, |
Perhaps it'd be nice to "convert" the knowledge/tips from this issue into a cookbook recipe? |
https://stackoverflow.com/a/56256513 does the job for me |
Hi all,
I was interested in seeing what it would take to hack together some options support (eventually in order to get fine-grained CORS support). This is what I came up with and I'd like some feedback.
First we provide the
Options
data type. It's parameterized by a list of allowed methodsAllowed methods arise from the standard types using another type
and then a type family allows us to automatically compute the
Options
type from the type of your APIThe server implementation is simple, too.
Options
does not support a body (though perhaps it could) so the implementation is trivialI don't choose to use
()
as the implementation since (a) this requires importing the module and thus sidesteps orphan instance issues when the API and implementation modules are separate like mine are and (b) this is more semantically obvious.Before describing
route
I need to be able to analyzeallowedMethods
so I writewith instances like
and now
route
is trivialSo with all this machinery out of the way, I can augment an API with
OPTIONS
support byThe text was updated successfully, but these errors were encountered: