英文:
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
函数更改为将 GET
和 PUT
都作为允许的方法返回,但是尽管这样会导致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("/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)
答案1
得分: 1
解决方案是添加另一个仅接受选项请求的端点,并在那里返回所有可能的其他方法(在这种情况下为GET
/PUT
),并从读取/更新端点的允许方法中删除OPTIONS
。
为什么gorilla会将第二个请求路由到第一个端点,该端点回答OPTIONS
请求(但仅接受GET
),而不是实际接受PUT
请求的端点?
- 第一个请求:来自浏览器的
OPTIONS
请求 - 第一个响应:允许的方法:
GET
,OPTIONS
(来自读取端点) - 第二个请求:来自浏览器的
PUT
请求 - 第二个响应:方法不允许(来自读取端点,该端点仅注册为接受
GET
和OPTIONS
)
英文:
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?
- 1st request:
OPTIONS
from browser - 1st response: allowed methods:
GET
,OPTIONS
(from read endpoint) - 2nd request:
PUT
from browser - 2nd response: method not allowed (from read endpoint, which is registered to take
GET
andOPTIONS
only)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论