英文:
Accessing HTTP Request context after handler
问题
在我的日志中间件(链中的第一个)中,我需要访问一些上下文信息,这些信息是在链中的某个身份验证中间件中编写的,并且仅在处理程序本身执行后才能访问。
附注:日志中间件需要首先调用,因为我需要记录请求的持续时间,包括在中间件中花费的时间。此外,身份验证中间件能够在权限不足时中止请求。在这种情况下,我还需要记录失败的请求。
我的问题是,从http.Request
指针中读取上下文不会返回我期望的身份验证数据。请参考下面的示例:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
const (
contextKeyUsername = "username"
)
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, contextKeyUsername, "user123")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func(start time.Time) {
ctx := r.Context()
username := ctx.Value(contextKeyUsername)
if username != nil {
fmt.Printf("user %s has accessed %s, took %d\n", username,
r.URL.Path, time.Since(start).Milliseconds())
} else {
fmt.Printf("annonyous has accessed %s, took %d\n",
r.URL.Path, time.Since(start).Milliseconds())
}
}(time.Now())
next.ServeHTTP(w, r)
})
}
func welcome(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
username := ctx.Value(contextKeyUsername)
if username != nil {
fmt.Fprintf(w, fmt.Sprintf("hello %s", username.(string)))
} else {
fmt.Fprintf(w, "hello")
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/welcome", welcome)
chain := logMiddleware(authMiddleware(mux))
http.ListenAndServe(":5050", chain)
}
虽然对127.0.0.1:5050/welcome
的GET请求确实返回了预期的字符串hello user123
,但日志的输出却是:
annonyous has accessed /welcome, took 0
由于请求被作为指针传递,我本来期望在执行延迟操作时,上下文中将包含预期的username
值。
我在这里漏掉了什么?
英文:
In my logging middleware (first in chain) I need to access some context that is written in some auth middleware futher down the chain and only after the handler itself is executed.
Side note: The logging middleware needs to be called first since I need to log the duration of the request including the time spend in middleware. Also the auth middleware is able to abort a request when permissions are not sufficient. in that case I need to log the failed request as well.
My problem with that is that reading the context from the http.Request
pointer does not return the auth data I would expect it to have. See the example bellow:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
const (
contextKeyUsername = "username"
)
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, contextKeyUsername, "user123")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func(start time.Time) {
ctx := r.Context()
username := ctx.Value(contextKeyUsername)
if username != nil {
fmt.Printf("user %s has accessed %s, took %d\n", username,
r.URL.Path, time.Since(start).Milliseconds())
} else {
fmt.Printf("annonyous has accessed %s, took %d\n",
r.URL.Path, time.Since(start).Milliseconds())
}
}(time.Now())
next.ServeHTTP(w, r)
})
}
func welcome(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
username := ctx.Value(contextKeyUsername)
if username != nil {
fmt.Fprintf(w, fmt.Sprintf("hello %s", username.(string)))
} else {
fmt.Fprintf(w, "hello")
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/welcome", welcome)
chain := logMiddleware(authMiddleware(mux))
http.ListenAndServe(":5050", chain)
}
While a get request to 127.0.0.1:5050/welcome
does return the expected string hello user123
, the output of the log is:
annonyous has accessed /welcome, took 0
Since the request is passed along as pointer I would have expected that at the time the defer is executed, the context would contain the expected username
value.
What am I missing here?
答案1
得分: 2
WithContext
返回请求的浅拷贝,即由 authMiddleware
创建的请求与 logMiddleware
从中读取上下文的请求不是同一个请求。
你可以让根中间件(在这种情况下是 logMiddleware
)创建带有值的上下文和浅拷贝的请求,但是不要存储一个普通字符串,而是在上下文中存储一个非空指针,然后让 authMiddleware
使用指针间接赋值给指针指向的值,然后在 next
退出后,logMiddleware
可以解引用该指针来访问该值。
为了避免不愉快的解引用,你可以使用指向具有字符串字段的结构体的指针。
type ctxKey uint8
const userKey ctxKey = 0
type user struct{ name string }
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := new(user)
r = r.WithContext(context.WithValue(r.Context(), userKey, u))
defer func(start time.Time) {
if u.name != "" {
fmt.Printf("用户 %s 已访问 %s,耗时 %s\n", u.name, r.URL.Path, time.Since(start))
} else {
fmt.Printf("匿名用户已访问 %s,耗时 %s\n", r.URL.Path, time.Since(start))
}
}(time.Now())
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey).(*user); ok {
u.name = "user123"
}
next.ServeHTTP(w, r)
})
}
func welcome(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey).(*user); ok && u.name != "" {
fmt.Fprintf(w, "你好 %s", u.name)
} else {
fmt.Fprintf(w, "你好")
}
}
https://go.dev/play/p/N7vmjQ7iLM1
英文:
WithContext
returns a shallow copy of the request, i.e. the request created by the authMiddleware
is not the same request as the one from which logMiddleware
is reading the context.
You could have the root middleware (in this case that would be the logMiddleware
) create the context-with-value and the shallow request copy, but instead of a plain string store an non-nil pointer in the context, then have the authMiddleware
use pointer indirection to assign the value to which the pointer points, then the logMiddleware
, after next
exits, can dereference that pointer to access that value.
And to avoid the unpleasant dereferencing, instead of a pointer to a string, you can use a pointer to a struct with a string field.
type ctxKey uint8
const userKey ctxKey = 0
type user struct{ name string }
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := new(user)
r = r.WithContext(context.WithValue(r.Context(), userKey, u))
defer func(start time.Time) {
if u.name != "" {
fmt.Printf("user %s has accessed %s, took %s\n", u.name, r.URL.Path, time.Since(start))
} else {
fmt.Printf("annonyous has accessed %s, took %s\n", r.URL.Path, time.Since(start))
}
}(time.Now())
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey).(*user); ok {
u.name = "user123"
}
next.ServeHTTP(w, r)
})
}
func welcome(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey).(*user); ok && u.name != "" {
fmt.Fprintf(w, "hello %s", u.name)
} else {
fmt.Fprintf(w, "hello")
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论