1

I've found a lot of CORS-related questions, but none of them solved my problem. As I'm quite new to web-dev, I'd appreciate any help with getting my project infrastructure up and running.

My application is made of two parts:

Server part

  • A server made with Haskell, Servant. The most important endpoint I've been trying to connect to is:
type Unprotected =
    "login" 
        :> ReqBody '[JSON] Credentials 
        :> Verb 'POST 204 '[JSON] (Headers '[Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] NoContent)

It's based on the project described here: https://github.com/haskell-servant/servant-auth

I've already found a SO question about CORS: CORS header ‘Access-Control-Allow-Origin’ missing in servant, but that didn't help.

During development I launch the server on port 8081.

UI Part

  • I have also a UI project, that's hosted independently on port 8833. It uses WebPack. I've also found some SO posts regarding CORS, advising to use:
  devServer: {
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": "true",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"
    }

That didn't help either.

Problem description:

When I make a "fetch" call in the UI project, trying to communicate with the Servant endpoint I get:

Access to fetch at 'http://localhost:8081/login' from origin 'http://localhost:8833' 
has been blocked by CORS policy: Response to preflight request doesn't pass access control
check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an 
opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource 
with CORS disabled.

The way I use fetch:

        let data = {
            credentialsUserName: this.state.login,
            credentialsPassword: this.state.password
        };

        let opts : RequestInit = { 
            method: "POST",
            headers: new Headers({ "Content-Type": "application/json" }),
            body: JSON.stringify(data)
        };

        fetch("http://localhost:8081/login", opts).then((value) => {
            console.log(value);
            // do something later
        });

Questions

What shall I do in order to make the fetch work fine with the servant endpoint. Also, is there anything wrong with my application architecture ? Maybe there's a better way to design it.

Last thing: The entire code of my application is quite long, so I decided to put only to most important parts here. Feel free to request more if needed.

Edit 1

After sending the fetch request I can see two entries in "Network" tab in my browser.

The first one with the following headers:

content-type: application/json
Referer: http://localhost:8833/Login
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36

and json data I included in its body. It's status is "(failed) net::ERR_FAILED".

The other's status is 400 and contains more headers:

Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Connection: keep-alive
Host: localhost:8081
Origin: http://localhost:8833
Referer: http://localhost:8833/Login
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36

Interestingly, In "General" tab I can see:

Request URL: http://localhost:8081/login
Request Method: OPTIONS
Status Code: 400 Bad Request
Remote Address: 127.0.0.1:8081
Referrer Policy: no-referrer-when-downgrade

Request Method "OPTIONS" looks mysterious. Why is it there ?

As a quick guess I tried adding "OPTIONS" to CORS definition on Haskell side, but no luck this time either:

port :: Int
port = 8081

corsPolicy :: Middleware
corsPolicy = cors (const $ Just policy)
    where
      policy = simpleCorsResourcePolicy
        { corsMethods = [ "GET", "POST", "PUT", "OPTIONS" ]}

main :: IO ()
main = do
    migrateDB
    jwtConfig <- getJwtConfig
    putStrLn $ "Serving endpoint " ++ (show port)
    run port $ corsPolicy $ serveWithContext proxy (context cookieConfig jwtConfig) (appAPI cookieConfig jwtConfig)
EDIT 2

Thanks for the help - I've managed to configure Servant, here it is:

corsPolicy :: Middleware
corsPolicy = cors (const $ Just policy)
    where
        policy = simpleCorsResourcePolicy
          { 
              corsMethods = [ "GET", "POST", "PUT", "OPTIONS" ],
              corsOrigins = Just (["http://localhost:8833"], True),
              corsRequestHeaders = [ "authorization", "content-type" ]
          }

The remaining part of Main as in my previous listing. By default Haskell CORS doesn't accept any headers. Not even "Content-Type", which I intend to send.

LA.27
  • 1,888
  • 19
  • 35
  • If your server uses `Wai` (which I assume it does) then you will probably want to use [this](https://hackage.haskell.org/package/wai-cors-0.2.7/docs/Network-Wai-Middleware-Cors.html) package – Robin Zigmond Sep 13 '20 at 22:59
  • What’s the HTTP status code of the response? You can use the Network pane in browser devtools to check. If Chrome devtools don’t show it, use the Network pane in Firefox devtools to check. Is it a 4xx or 5xx error rather than a 200 success response? – sideshowbarker Sep 13 '20 at 23:14
  • @sideshowbarker good point - Interestingly enough, I can see two entries - one with status "(failed) net::ERR_FAILED", and another with "400 (Bad Request)". That suggests there's a problem with the UI / WebPack configuration. Let me add more details about the requests in my main post. – LA.27 Sep 13 '20 at 23:37
  • 1
    Yeah based on that info, I’d say there’s not actually a CORS problem. Instead those failures end up causing the browser to either receive no response at all (In the ERR_FAILED), or to receive a response lacking the Access-Control-Allow-Origin response header (in the 400 Bad Request case, it’s normal and expected that the response won’t have the Access-Control-Allow-Origin response header). So at this point you probably want to consider either completely rewriting this question—to re-focus it on the actual problems, rather than CORS (which isn’t the cause)—or else post a new separate question. – sideshowbarker Sep 13 '20 at 23:43
  • OK thanks, let me think about it for a while and I'll come back here later. – LA.27 Sep 13 '20 at 23:50
  • 1
    The OPTIONS request is there because adding a `content-type: application/json` request header triggers browsers to send a CORS preflight OPTIONS request before trying the actual POST request from your code. And if the that preflight request fails, the browser stops right there and never moves on to trying the POST request. And the reason the CORS preflight is failing is, the response is a 400 error. For the preflight to succeed, the respond must be a 200 OK. So you need to configure the `http://localhost:8081/login'` server to respond to OPTIONS request with a 200 status code rather than 400. – sideshowbarker Sep 14 '20 at 01:57
  • Makes a lot of sense. Thanks. Let me try it. – LA.27 Sep 15 '20 at 12:56
  • @sideshowbarker - Thanks ! I found a solution. Can you respond to my question so that I can mark your response as accepted ? – LA.27 Sep 15 '20 at 15:05

0 Answers0