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)