3

I am using Golang with gin-gonic/gin web framework in my backend and with React axios in my frontend. I tried to solve it for two days already, but I still get the same error below:

CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This error happens only when I try to send the PATCH request, so the one which requires the preflight OPTIONS request, but everything works as expected for GET and POST, which do not run any preflight checks.

Here is the code for my router configuration:

package main

import (
    "book_renting/api"
    "log"
    "net/http"

    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/contrib/cors"
    "github.com/gin-gonic/gin"
    _ "github.com/lib/pq"
)

func main() {

    router := gin.Default()
    store := cookie.NewStore([]byte("your-secret-key"))
    store.Options(sessions.Options{MaxAge: 60 * 60 * 24})

    router.Use(cors.Default())
    router.Use(sessions.Sessions("sessions", store))

    router.Use(func(c *gin.Context) {
        host := c.Request.Header.Get("Origin")
        c.Writer.Header().Set("Access-Control-Allow-Origin", host)
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
        if c.Request.Method == "OPTIONS" {
            log.Println("Handling OPTIONS request")
            c.AbortWithStatus(http.StatusNoContent)
            return
        }
        log.Println("Executing CORS middleware")
        c.Next()
    })

    router.POST("/login", api.HandleLogin)
    router.GET("/logout", api.HandleLogout)
    router.POST("/register", api.HandleRegister)
    router.GET("/getCookie", api.GetCookieSession)

    router.GET("/books", api.GetBooksAPI)
    router.GET("/books/:id", api.BookByIdAPI)
    router.PATCH("/rent/:id", api.RentBookAPI)
    router.PATCH("/return/:id", api.ReturnBookAPI)
    router.Run("localhost:3000")
}

And here is the frontend side:

import axios from 'axios'

const url = 'http://localhost:3000'

export const loginUser = async (credentials) => await axios.post(`${url}/login`, credentials, {withCredentials: true})
export const logoutUser = async () => await axios.get(`${url}/logout`, {withCredentials: true})
export const registerUser = () => axios.post(`${url}/register`)
export const fetchBooks = () => axios.get(`${url}/books`, { withCredentials: true })
export const fetchBookByID = (book_id) => axios.get(`${url}/books/${book_id}`, { withCredentials: true })
export const rentBook = (book_id) => axios.patch(`${url}/rent/${book_id}`, { withCredentials: true })
export const returnBook = (book_id) => axios.patch(`${url}/return/${book_id}`, { withCredentials: true })

I am quite certain that I set up the backend side properly, that it should return all necessary headers.

For example for the GET request the response headers look like this:

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Access-Control-Allow-Origin: http://localhost:3001
Content-Type: application/json; charset=utf-8
Date: Sat, 10 Jun 2023 22:12:11 GMT
Content-Length: 495

While for the PATCH request attempt I do not have any response (not surprisingly) and the preflight response headers are:

HTTP/1.1 200 OK
Date: Sat, 10 Jun 2023 22:12:12 GMT
Content-Length: 0

Do you have any suggestions what could be the issue? After these two days I am already clueless. Thank you in advance!

I also tried to put headers:

c.Writer.Header().Set("Access-Control-Allow-Origin", host)
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")

...again in the if statement:

if c.Request.Method == "OPTIONS" {
    log.Println("Handling OPTIONS request")
    c.AbortWithStatus(http.StatusNoContent)
    return
    }

But it didn't help at all. In fact, this if statement is not executed when the preflight is being performed and I know from the console that the server is executing the OPTIONS request.

[GIN] 2023/06/11 - 00:12:13 | 200 |       7.708µs |       127.0.0.1 | OPTIONS  "/rent/2"

EDIT:

Here is the cURL command sending the PATCH request (so in fact here is the preflight OPTIONS request):

curl 'http://localhost:3000/return/2' \
  -X 'OPTIONS' \
  -H 'Accept: */*' \
  -H 'Accept-Language: en-US,en;q=0.9,pl-PL;q=0.8,pl;q=0.7' \
  -H 'Access-Control-Request-Headers: content-type' \
  -H 'Access-Control-Request-Method: PATCH' \
  -H 'Cache-Control: no-cache' \
  -H 'Connection: keep-alive' \
  -H 'Origin: http://localhost:3001' \
  -H 'Pragma: no-cache' \
  -H 'Referer: http://localhost:3001/' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-site' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36' \
  --compressed

And the response to this request:

HTTP/1.1 200 OK
Date: Sun, 11 Jun 2023 01:22:57 GMT
Content-Length: 0
Vertislav
  • 33
  • 5
  • Please share the request (copy as cURL) and the response (execute the copied command in the last step) of the problematic preflight request. – Zeke Lu Jun 10 '23 at 23:00
  • @ZekeLu I am not 100% if I got you right, but here is the cURL I got from Postman: curl --location --request OPTIONS 'http://localhost:3000/rent/2' \ --header 'Cookie: sessions=MTY4' Speaking of the response to this command, the body is empty. The headers I will paste-in the answer below. What I want to stress out, from the Postman everything works fine, it behaves as it is supposed to. The problem appears when I want to execute PATCH requests from the browser. – Vertislav Jun 11 '23 at 01:12
  • [ { "key": "Access-Control-Allow-Credentials", "value": "true" }, { "key": "Access-Control-Allow-Headers", "value": "Content-Type, Authorization" }, { "key": "Access-Control-Allow-Methods", "value": "GET, POST, PUT, DELETE, PATCH, OPTIONS" }, { "key": "Access-Control-Allow-Origin", "value": "" }, { "key": "Date", "value": "Sun, 11 Jun 2023 01:04:15 GMT" } ] – Vertislav Jun 11 '23 at 01:14
  • Please copy the request from the DevTools of the browser. See [this answer](https://stackoverflow.com/a/75935458/1369400) for the details. We don't know what's wrong yet. We need to narrow down the issue. It could be a server issue, or a client issue, or a browser issue (caching). – Zeke Lu Jun 11 '23 at 01:18
  • Great, thanks for the tip. cURL: curl 'http://localhost:3000/return/2' \ -X 'OPTIONS' \ -H 'Accept: */*' \ -H 'Accept-Language: en-US,en;q=0.9,pl-PL;q=0.8,pl;q=0.7' \ -H 'Access-Control-Request-Headers: content-type' \ -H 'Access-Control-Request-Method: PATCH' \ -H 'Cache-Control: no-cache' \ -H 'Connection: keep-alive' \ -H 'Origin: http://localhost:3001' \ -H 'Pragma: no-cache' \ -H 'Referer: http://localhost:3001/' \ -H 'Sec-Fetch-Dest: empty' \ -H 'Sec-Fetch-Mode: cors' \ -H 'Sec-Fetch-Site: same-site' \ – Vertislav Jun 11 '23 at 01:24
  • -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36' \ --compressed – Vertislav Jun 11 '23 at 01:24
  • And the response: HTTP/1.1 200 OK Date: Sun, 11 Jun 2023 01:22:57 GMT Content-Length: 0 – Vertislav Jun 11 '23 at 01:25
  • I added to my original post at the end. – Vertislav Jun 11 '23 at 01:40
  • 1
    Thank you! This is a server side issue. It should work by commenting out `router.Use(cors.Default())`. I'm investing what's wrong with the middleware. – Zeke Lu Jun 11 '23 at 01:48
  • @ZekeLu Yup, that helps! Now CORS doesn't block the request. Thank you very much! Now I got a 401 error that I am unauthorized to execute the PATCH request, but I already see I lack a cookie header in my request header. Do you know by any chance how is that possible? – Vertislav Jun 11 '23 at 01:56
  • 1
    You're using the wrong package. `github.com/gin-gonic/contrib/cors` is deprecated. Use `github.com/gin-contrib/cors` instead. – Zeke Lu Jun 11 '23 at 02:07
  • `Now I got a 401 error that I am unauthorized to execute the PATCH request`, the cors middleware should be placed before the session middleware. – Zeke Lu Jun 11 '23 at 02:09

3 Answers3

1

It turns out that you're using the deprecated package github.com/gin-gonic/contrib/cors. You should use github.com/gin-contrib/cors instead. Here is a demo configuration to use github.com/gin-contrib/cors:

package main

import (
    "github.com/gin-contrib/cors"
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    config := cors.DefaultConfig()
    config.AddAllowHeaders("Authorization")
    config.AllowCredentials = true
    config.AllowAllOrigins = false
    // I think you should whitelist a limited origins instead:
    //  config.AllowAllOrigins = []{"xxxx", "xxxx"}
    config.AllowOriginFunc = func(origin string) bool {
        return true
    }
    router.Use(cors.New(config))

    store := cookie.NewStore([]byte("your-secret-key"))
    store.Options(sessions.Options{MaxAge: 60 * 60 * 24})
    router.Use(sessions.Sessions("sessions", store))

    // routes below

    router.Run("localhost:3000")
}

The PATCH request header for some reason lacks the "Cookie" header, despite the fact that I use the withCredentials parameter.

axios.patch(`${url}/rent/${book_id}`, { withCredentials: true })

Here { withCredentials: true } is treated as the data, and there is not config. If you don't have data to send to the server, you should write it like this:

axios.patch(`${url}/rent/${book_id}`, null, { withCredentials: true })
Zeke Lu
  • 6,349
  • 1
  • 17
  • 23
  • I implemented changes as you suggested, and while the original problem is gone for good, the authorization error (401) still persists. The PATCH request header for some reason lacks the "Cookie" header, despite the fact that I use the withCredentials parameter. I will work on solving that behavior later today. If I will find a solution I will post it in this thread. Thank you for your help! – Vertislav Jun 11 '23 at 08:12
  • `The PATCH request header for some reason lacks the "Cookie" header`. How about the `POST` and `GET` requests? – Zeke Lu Jun 11 '23 at 08:31
  • They have it, I checked all other requests and they carry the "Cookie" header. I looked now and noticed, that the `POST` header not only lacks the Cookie header but carries a `--data-raw '{"withCredentials":true}'` parameter instead. But this param is not present in `GET` request. Probably on my front end side I have something wrong, I will investigate it further. EDIT: I also executed the cURL command where I put the cookie header to the `PATCH` request and it worked. So definitely lacking header is the key. – Vertislav Jun 11 '23 at 08:50
  • See the updated answer. `{"withCredentials":true}` is treated as the data. That's why it does not work. – Zeke Lu Jun 11 '23 at 09:17
  • Yes it works. Now it does everything as it is supposed to. Thank you very much for all your help, tips and effort, I appreciate that! – Vertislav Jun 11 '23 at 09:25
  • 1
    I'm glad that it helps! Thanks for working together to address the issue! – Zeke Lu Jun 11 '23 at 09:28
0

While it seems that a 204 response would be appropriate here, the specification is a bit unclear, so you may need to return a 200 for the options request to make CORS work in all browsers.

GreyBeardedGeek
  • 29,460
  • 2
  • 47
  • 67
0

what you have done in the middleware seems enough and with no issues. since the CORS are applying to other methods.

have you handled all the errors in this specific "api.ReturnBookAPI" handler?

if a request got error and it wouldn't return the gin response appropriately, the CORS headers wouldn't be reflected in the response headers.

Content-Length: 0 this field in your res shows there are no response body at all

check the gin service log output, and ensure you have printed and checked all the potential errors.