为什么 Gorilla Mux 在 Firefox 和 Thunder Client 中以不同的方式路由我的请求?

huangapple go评论75阅读模式
英文:

Why does gorillamux route my request differently in firefox VS thunder client?

问题

我有一个使用Golang编写的小型Web应用程序,使用gorillamux配置了一些路由,路径完全相同,但接受不同的方法,就像这样(简化):

r.HandleFunc("/users", sendPreflightHeaders("GET", readHandler())).Methods("GET","OPTIONS")
r.HandleFunc("/users", sendPreflightHeaders("PUT", updateHandler())).Methods("PUT","OPTIONS")

sendPreflightHeaders() 函数通过头部返回给定端点的允许方法(包括 OPTIONS)。

当我在Thunder Client(VScode)中进行PUT请求时,我得到了以下预期的成功响应:

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

然而,当我在运行在 localhost:3000 上的React应用程序中使用Firefox进行相同的 fetch() 请求时,我得到了以下预检请求的响应:

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

而随后的 PUT 请求失败了:

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').

显然,它将路由到 readHandler() 而不是 updateHandler()。但是当我在Thunder Client中发送请求时,一切都正常...而且我认为Firefox发送的请求与Thunder Client之间没有太大的区别,但我会将它们包含在内,因为它们可能是相关的。

Thunder Client 发送的 PUT 请求:

Headers:
Accept: */*
User-Agent: Thunder Client (https://www.thunderclient.com)
authorization: Bearer <...>

浏览器发送的 CORS 预检请求:

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 请求:

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

我还尝试将 sendPreflightHeaders 函数更改为将 GETPUT 都作为允许的方法返回,但是尽管这样会导致PUT请求成功,但实际上 /users 服务将 PUT 请求解释为读取请求。

当我在浏览器中进行此请求时,它似乎将其路由到读取处理程序而不是更新处理程序。但在Thunder Client中没有这个问题。

为什么gorilla会对浏览器(Firefox)的请求和Thunder Client的请求返回不同的响应?

最小可复现示例:(React后端)fetch() 代码

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)
    }

最小可复现示例:(Go后端)

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)
	})
}

使用这个最小示例,我得到了相同的结果。输出是:

read
read

(更新处理程序似乎根本没有被调用)

英文:

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(&quot;/users&quot;, sendPreflightHeaders( &quot;GET&quot;, readHandler()   )).Methods(&quot;GET&quot;,&quot;OPTIONS&quot;)
r.HandleFunc(&quot;/users&quot;, sendPreflightHeaders( &quot;PUT&quot;, updateHandler() )).Methods(&quot;PUT&quot;,&quot;OPTIONS&quot;)

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: &lt;...&gt;
content-length: &lt;...&gt;
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: &lt;...&gt;
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 &lt;...&gt;

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 &lt;...&gt;
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 &quot;GET&quot; and &quot;PUT&quot; 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) =&gt; setTimeout(resolve, delay))
}
const fetchRetry = async(url, delay, tries, timeout, fetchOptions = {}) =&gt; {
function onError(err) {
let triesLeft = tries - 1
if(!triesLeft) {
throw err
}
return wait(delay).then(() =&gt; fetchRetry(url, delay, triesLeft, timeout, fetchOptions))
}
const controller = new AbortController()
const id = setTimeout(() =&gt; controller.abort(), timeout)
const response = await fetch(url,{...fetchOptions, signal: controller.signal}).catch(onError)
clearTimeout(id)
return response
}
function updateUser(token, user) {
let url = &#39;http://localhost:8008/users&#39;
let options = {
headers: new Headers({
&quot;Access-Control-Allow-Credentials&quot;: &quot;true&quot;,
&#39;Authorization&#39;: &#39;Bearer &#39; + token,
&#39;Content-Type&#39;: &#39;application/json&#39;
}),
method: &#39;PUT&#39;,
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 = &#39;http://localhost:8008/users&#39;
let options = {
headers: new Headers({
&quot;Access-Control-Allow-Credentials&quot;: &quot;true&quot;,
&#39;Authorization&#39;: &#39;Bearer &#39; + 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 (
&quot;fmt&quot;
&quot;net/http&quot;
&quot;strings&quot;
&quot;github.com/gorilla/mux&quot;
)
func main() {
port := 8008
r := mux.NewRouter()
path := &quot;/users&quot;
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(&quot;:%d&quot;, port), r)
}
func readHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(&quot;read&quot;)
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(&quot;update&quot;)
}
func sendPreflightHeaders(allowedMethods []string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(&quot;Access-Control-Allow-Origin&quot;, &quot;http://localhost:3000&quot;)
methodIsValid := false
for _, method := range allowedMethods {
if r.Method == method {
methodIsValid = true
}
}
if !methodIsValid {
allowedMethodsList := strings.Join(allowedMethods, &quot; or&quot;)
w.Header().Set(&quot;Access-Control-Allow-Methods&quot;, allowedMethodsList)
return
}
w.Header().Set(&quot;Access-Control-Allow-Methods&quot;, strings.Join(allowedMethods, &quot;, &quot;))
w.Header().Set(&quot;Access-Control-Allow-Headers&quot;, &quot;Authorization, Content-Type, Access-Control-Allow-Credentials, Access-Control-Allow-Origin&quot;)
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)

答案1

得分: 1

解决方案是添加另一个仅接受选项请求的端点,并在那里返回所有可能的其他方法(在这种情况下为GET/PUT),并从读取/更新端点的允许方法中删除OPTIONS

为什么gorilla会将第二个请求路由到第一个端点,该端点回答OPTIONS请求(但仅接受GET),而不是实际接受PUT请求的端点?

  1. 第一个请求:来自浏览器的OPTIONS请求
  2. 第一个响应:允许的方法:GETOPTIONS(来自读取端点)
  3. 第二个请求:来自浏览器的PUT请求
  4. 第二个响应:方法不允许(来自读取端点,该端点仅注册为接受GETOPTIONS
英文:

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 (
&quot;fmt&quot;
&quot;net/http&quot;
&quot;strings&quot;
&quot;github.com/gorilla/mux&quot;
)
func main() {
port := 8008
r := mux.NewRouter()
path := &quot;/users&quot;
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(&quot;:%d&quot;, port), r)
}
func optionsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(&quot;options&quot;)
}
func readHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(&quot;read&quot;)
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(&quot;update&quot;)
}
func sendPreflightHeaders(allowedMethods []string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(&quot;Access-Control-Allow-Origin&quot;, &quot;http://localhost:3000&quot;)
methodIsValid := false
for _, method := range allowedMethods {
if r.Method == method {
methodIsValid = true
}
}
if !methodIsValid {
allowedMethodsList := strings.Join(allowedMethods, &quot; or&quot;)
w.Header().Set(&quot;Access-Control-Allow-Methods&quot;, allowedMethodsList)
return
}
w.Header().Set(&quot;Access-Control-Allow-Methods&quot;, strings.Join(allowedMethods, &quot;, &quot;))
w.Header().Set(&quot;Access-Control-Allow-Headers&quot;, &quot;Authorization, Content-Type, Access-Control-Allow-Credentials, Access-Control-Allow-Origin&quot;)
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?

  1. 1st request: OPTIONS from browser
  2. 1st response: allowed methods: GET, OPTIONS (from read endpoint)
  3. 2nd request: PUT from browser
  4. 2nd response: method not allowed (from read endpoint, which is registered to take GET and OPTIONS only)

huangapple
  • 本文由 发表于 2022年4月12日 12:44:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/71837219.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定