Reputation: 327
I have a small web application written in golang with a few routes configured in gorillamux, with the exact same path but accepting different methods, like so (simplified):
r.HandleFunc("/users", sendPreflightHeaders( "GET", readHandler() )).Methods("GET","OPTIONS")
r.HandleFunc("/users", sendPreflightHeaders( "PUT", updateHandler() )).Methods("PUT","OPTIONS")
The sendPreflightHeaders()
function sends back via header the allowed methods for the given endpoint. (plus OPTIONS
)
When I do a PUT request in Thunder Client (VScode), I get the following successful response (as I would expect):
access-control-allow-headers: Authorization, Content-Type, Access-Control-Allow-Credentials, Access-Control-Allow-Origin
access-control-allow-methods: PUT, OPTIONS
access-control-allow-origin: http://localhost:3000
date: <...>
content-length: <...>
content-type: text/plain; charset=utf-8
connection: close
However, when I do the same request as a fetch()
request in the react app I have running at localhost:3000
using Firefox, I get the following response to the preflight request:
HTTP/1.1 200 OK
Access-Control-Allow-Headers: Authorization, Content-Type, Access-Control-Allow-Credentials, Access-Control-Allow-Origin
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Origin: http://localhost:3000
Date: <...>
Content-Length: 0
And the subsequent PUT
request comes back as a failure:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8001/users. (Reason: Did not find method in CORS header ‘Access-Control-Allow-Methods’).
Clearly, it is routing to the readHandler()
and not the updateHandler()
. But this works just fine when I send the request in thunder client...and I don't think there is a major difference in the request being sent by firefox VS thunder client, but I will include them as they may be relevant.
PUT Request sent by thunder client:
Headers:
Accept: */*
User-Agent: Thunder Client (https://www.thunderclient.com)
authorization: Bearer <...>
CORS Preflight Request sent by browser:
OPTIONS /users HTTP/1.1
Host: localhost:8001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: access-control-allow-credentials,authorization,content-type
Referer: http://localhost:3000/
Origin: http://localhost:3000
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
DNT: 1
Sec-GPC: 1
PUT request sent by browser:
PUT /users undefined
Host: localhost:8001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:3000/
access-control-allow-credentials: true
authorization: Bearer <...>
content-type: application/json
Origin: http://localhost:3000
Content-Length: 59
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
DNT: 1
Sec-GPC: 1
I have also tried changing the sendPreflightHeaders
function to send back both "GET"
and "PUT"
as allowed methods, but while that results in the PUT request succeeding, the PUT
request is actually being interpreted as a read request by the /users
service.
It would seem that when I make this request in the browser, it is being routed to the read handler rather than the update handler. But not in thunder client.
Why does gorilla return a different response from a request by the browser (firefox) VS a request by thunder client?
Minimal reproducible example: (react backend) fetch() code
function wait(delay) {
return new Promise((resolve) => setTimeout(resolve, delay))
}
const fetchRetry = async(url, delay, tries, timeout, fetchOptions = {}) => {
function onError(err) {
let triesLeft = tries - 1
if(!triesLeft) {
throw err
}
return wait(delay).then(() => fetchRetry(url, delay, triesLeft, timeout, fetchOptions))
}
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeout)
const response = await fetch(url,{...fetchOptions, signal: controller.signal}).catch(onError)
clearTimeout(id)
return response
}
function updateUser(token, user) {
let url = 'http://localhost:8008/users'
let options = {
headers: new Headers({
"Access-Control-Allow-Credentials": "true",
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}),
method: 'PUT',
body: JSON.stringify(user),
}
let retryDelayMs = 3000
let tries = 1
let timeout = 3000
return fetchRetryResponse(url, retryDelayMs, tries, timeout, options)
}
function getUser(token, userId) {
let url = 'http://localhost:8008/users'
let options = {
headers: new Headers({
"Access-Control-Allow-Credentials": "true",
'Authorization': 'Bearer ' + token
})
}
let waitMs = 3000
let retries = 3
let timeout = 3000
return fetchRetryResponse(url, waitMs, retries, timeout, options)
}
Minimal reproducible example: (go backend)
package main
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func main() {
port := 8008
r := mux.NewRouter()
path := "/users"
readMethods := []string{http.MethodGet, http.MethodOptions}
updateMethods := []string{http.MethodPut, http.MethodOptions}
r.HandleFunc(path, sendPreflightHeaders(readMethods, readHandler)).Methods(readMethods...)
r.HandleFunc(path, sendPreflightHeaders(updateMethods, updateHandler)).Methods(updateMethods...)
http.ListenAndServe(fmt.Sprintf(":%d", port), r)
}
func readHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("read")
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("update")
}
func sendPreflightHeaders(allowedMethods []string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
methodIsValid := false
for _, method := range allowedMethods {
if r.Method == method {
methodIsValid = true
}
}
if !methodIsValid {
allowedMethodsList := strings.Join(allowedMethods, " or")
w.Header().Set("Access-Control-Allow-Methods", allowedMethodsList)
return
}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Access-Control-Allow-Credentials, Access-Control-Allow-Origin")
if r.Method == http.MethodOptions {
return // is a preflight request
}
next.ServeHTTP(w, r)
})
}
Using this minimal example, I get the same results. Output is:
read
read
(update handler doesn't seem to be reached at all)
Upvotes: 2
Views: 224
Reputation: 327
The solution was to add another endpoint that only takes options requests, and to return all the possible other methods (GET
/PUT
in this case) there, and to remove OPTIONS
from the allowed methods for the read/update endpoints.
package main
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
)
func main() {
port := 8008
r := mux.NewRouter()
path := "/users"
readMethods := []string{http.MethodGet}
updateMethods := []string{http.MethodPut}
allMethods := []string{http.MethodGet, http.MethodPut}
r.HandleFunc(path, sendPreflightHeaders(readMethods, readHandler)).Methods(readMethods...)
r.HandleFunc(path, sendPreflightHeaders(updateMethods, updateHandler)).Methods(updateMethods...)
r.HandleFunc(path, sendPreflightHeaders(allMethods, optionsHandler)).Methods(http.MethodOptions)
http.ListenAndServe(fmt.Sprintf(":%d", port), r)
}
func optionsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("options")
}
func readHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("read")
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("update")
}
func sendPreflightHeaders(allowedMethods []string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
methodIsValid := false
for _, method := range allowedMethods {
if r.Method == method {
methodIsValid = true
}
}
if !methodIsValid {
allowedMethodsList := strings.Join(allowedMethods, " or")
w.Header().Set("Access-Control-Allow-Methods", allowedMethodsList)
return
}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(allowedMethods, ", "))
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Access-Control-Allow-Credentials, Access-Control-Allow-Origin")
if r.Method == http.MethodOptions {
return // is a preflight request
}
next.ServeHTTP(w, r)
})
}
Output is:
options
read
update
However, I will mark another answer as accepted if they can tell me why gorilla/cors behaves this way.
Why would gorilla route the second request to the first endpoint that answers the OPTIONS
request (but only accepts GET
) rather than the endpoint that actually accepts the PUT
request?
OPTIONS
from browserGET
, OPTIONS
(from read endpoint)PUT
from browserGET
and OPTIONS
only)Upvotes: 1