mirror of https://github.com/go-gitea/gitea
Refactor web route (#24080)
The old code is unnecessarily complex, and has many misuses. Old code "wraps" a lot, wrap wrap wrap, it's difficult to understand which kind of handler is used. The new code uses a general approach, we do not need to write all kinds of handlers into the "wrapper", do not need to wrap them again and again. New code, there are only 2 concepts: 1. HandlerProvider: `func (h any) (handlerProvider func (next) http.Handler)`, it can be used as middleware 2. Use HandlerProvider to get the final HandlerFunc, and use it for `r.Get()` And we can decouple the route package from context package (see the TODO). # FAQ ## Is `reflect` safe? Yes, all handlers are checked during startup, see the `preCheckHandler` comment. If any handler is wrong, developers could know it in the first time. ## Does `reflect` affect performance? No. https://github.com/go-gitea/gitea/pull/24080#discussion_r1164825901 1. This reflect code only runs for each web handler call, handler is far more slower: 10ms-50ms 2. The reflect is pretty fast (comparing to other code): 0.000265ms 3. XORM has more reflect operations alreadypull/24242/head
parent
70fc47a22a
commit
b9a97ccd0e
@ -0,0 +1,200 @@ |
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package web |
||||
|
||||
import ( |
||||
goctx "context" |
||||
"fmt" |
||||
"net/http" |
||||
"reflect" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/web/routing" |
||||
) |
||||
|
||||
// ResponseStatusProvider is an interface to check whether the response has been written by the handler
|
||||
type ResponseStatusProvider interface { |
||||
Written() bool |
||||
} |
||||
|
||||
// TODO: decouple this from the context package, let the context package register these providers
|
||||
var argTypeProvider = map[reflect.Type]func(req *http.Request) ResponseStatusProvider{ |
||||
reflect.TypeOf(&context.APIContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetAPIContext(req) }, |
||||
reflect.TypeOf(&context.Context{}): func(req *http.Request) ResponseStatusProvider { return context.GetContext(req) }, |
||||
reflect.TypeOf(&context.PrivateContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetPrivateContext(req) }, |
||||
} |
||||
|
||||
// responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written
|
||||
type responseWriter struct { |
||||
respWriter http.ResponseWriter |
||||
status int |
||||
} |
||||
|
||||
var _ ResponseStatusProvider = (*responseWriter)(nil) |
||||
|
||||
func (r *responseWriter) Written() bool { |
||||
return r.status > 0 |
||||
} |
||||
|
||||
func (r *responseWriter) Header() http.Header { |
||||
return r.respWriter.Header() |
||||
} |
||||
|
||||
func (r *responseWriter) Write(bytes []byte) (int, error) { |
||||
if r.status == 0 { |
||||
r.status = http.StatusOK |
||||
} |
||||
return r.respWriter.Write(bytes) |
||||
} |
||||
|
||||
func (r *responseWriter) WriteHeader(statusCode int) { |
||||
r.status = statusCode |
||||
r.respWriter.WriteHeader(statusCode) |
||||
} |
||||
|
||||
var ( |
||||
httpReqType = reflect.TypeOf((*http.Request)(nil)) |
||||
respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() |
||||
cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() |
||||
) |
||||
|
||||
// preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup
|
||||
func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { |
||||
hasStatusProvider := false |
||||
for _, argIn := range argsIn { |
||||
if _, hasStatusProvider = argIn.Interface().(ResponseStatusProvider); hasStatusProvider { |
||||
break |
||||
} |
||||
} |
||||
if !hasStatusProvider { |
||||
panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type())) |
||||
} |
||||
if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 { |
||||
panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type())) |
||||
} |
||||
if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType { |
||||
panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type())) |
||||
} |
||||
} |
||||
|
||||
func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect.Value) []reflect.Value { |
||||
isPreCheck := req == nil |
||||
|
||||
argsIn := make([]reflect.Value, fn.Type().NumIn()) |
||||
for i := 0; i < fn.Type().NumIn(); i++ { |
||||
argTyp := fn.Type().In(i) |
||||
switch argTyp { |
||||
case respWriterType: |
||||
argsIn[i] = reflect.ValueOf(resp) |
||||
case httpReqType: |
||||
argsIn[i] = reflect.ValueOf(req) |
||||
default: |
||||
if argFn, ok := argTypeProvider[argTyp]; ok { |
||||
if isPreCheck { |
||||
argsIn[i] = reflect.ValueOf(&responseWriter{}) |
||||
} else { |
||||
argsIn[i] = reflect.ValueOf(argFn(req)) |
||||
} |
||||
} else { |
||||
panic(fmt.Sprintf("unsupported argument type: %s", argTyp)) |
||||
} |
||||
} |
||||
} |
||||
return argsIn |
||||
} |
||||
|
||||
func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { |
||||
if len(ret) == 1 { |
||||
if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok { |
||||
return cancelFunc |
||||
} |
||||
panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type())) |
||||
} else if len(ret) > 1 { |
||||
panic(fmt.Sprintf("unsupported return values: %s", fn.Type())) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func hasResponseBeenWritten(argsIn []reflect.Value) bool { |
||||
for _, argIn := range argsIn { |
||||
if statusProvider, ok := argIn.Interface().(ResponseStatusProvider); ok { |
||||
if statusProvider.Written() { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// toHandlerProvider converts a handler to a handler provider
|
||||
// A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware
|
||||
func toHandlerProvider(handler any) func(next http.Handler) http.Handler { |
||||
if hp, ok := handler.(func(next http.Handler) http.Handler); ok { |
||||
return hp |
||||
} |
||||
|
||||
funcInfo := routing.GetFuncInfo(handler) |
||||
fn := reflect.ValueOf(handler) |
||||
if fn.Type().Kind() != reflect.Func { |
||||
panic(fmt.Sprintf("handler must be a function, but got %s", fn.Type())) |
||||
} |
||||
|
||||
provider := func(next http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { |
||||
// wrap the response writer to check whether the response has been written
|
||||
resp := respOrig |
||||
if _, ok := resp.(ResponseStatusProvider); !ok { |
||||
resp = &responseWriter{respWriter: resp} |
||||
} |
||||
|
||||
// prepare the arguments for the handler and do pre-check
|
||||
argsIn := prepareHandleArgsIn(resp, req, fn) |
||||
if req == nil { |
||||
preCheckHandler(fn, argsIn) |
||||
return // it's doing pre-check, just return
|
||||
} |
||||
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ret := fn.Call(argsIn) |
||||
|
||||
// handle the return value, and defer the cancel function if there is one
|
||||
cancelFunc := handleResponse(fn, ret) |
||||
if cancelFunc != nil { |
||||
defer cancelFunc() |
||||
} |
||||
|
||||
// if the response has not been written, call the next handler
|
||||
if next != nil && !hasResponseBeenWritten(argsIn) { |
||||
next.ServeHTTP(resp, req) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
provider(nil).ServeHTTP(nil, nil) // do a pre-check to make sure all arguments and return values are supported
|
||||
return provider |
||||
} |
||||
|
||||
// MiddlewareWithPrefix wraps a handler function at a prefix, and make it as a middleware
|
||||
// TODO: this design is incorrect, the asset handler should not be a middleware
|
||||
func MiddlewareWithPrefix(pathPrefix string, middleware func(handler http.Handler) http.Handler, handlerFunc http.HandlerFunc) func(next http.Handler) http.Handler { |
||||
funcInfo := routing.GetFuncInfo(handlerFunc) |
||||
handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
handlerFunc(resp, req) |
||||
}) |
||||
return func(next http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
||||
if !strings.HasPrefix(req.URL.Path, pathPrefix) { |
||||
next.ServeHTTP(resp, req) |
||||
return |
||||
} |
||||
if middleware != nil { |
||||
middleware(handler).ServeHTTP(resp, req) |
||||
} else { |
||||
handler.ServeHTTP(resp, req) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -1,116 +0,0 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package web |
||||
|
||||
import ( |
||||
goctx "context" |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/web/routing" |
||||
) |
||||
|
||||
// Wrap converts all kinds of routes to standard library one
|
||||
func Wrap(handlers ...interface{}) http.HandlerFunc { |
||||
if len(handlers) == 0 { |
||||
panic("No handlers found") |
||||
} |
||||
|
||||
ourHandlers := make([]wrappedHandlerFunc, 0, len(handlers)) |
||||
|
||||
for _, handler := range handlers { |
||||
ourHandlers = append(ourHandlers, convertHandler(handler)) |
||||
} |
||||
return wrapInternal(ourHandlers) |
||||
} |
||||
|
||||
func wrapInternal(handlers []wrappedHandlerFunc) http.HandlerFunc { |
||||
return func(resp http.ResponseWriter, req *http.Request) { |
||||
var defers []func() |
||||
defer func() { |
||||
for i := len(defers) - 1; i >= 0; i-- { |
||||
defers[i]() |
||||
} |
||||
}() |
||||
for i := 0; i < len(handlers); i++ { |
||||
handler := handlers[i] |
||||
others := handlers[i+1:] |
||||
done, deferrable := handler(resp, req, others...) |
||||
if deferrable != nil { |
||||
defers = append(defers, deferrable) |
||||
} |
||||
if done { |
||||
return |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Middle wrap a context function as a chi middleware
|
||||
func Middle(f func(ctx *context.Context)) func(next http.Handler) http.Handler { |
||||
funcInfo := routing.GetFuncInfo(f) |
||||
return func(next http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetContext(req) |
||||
f(ctx) |
||||
if ctx.Written() { |
||||
return |
||||
} |
||||
next.ServeHTTP(ctx.Resp, ctx.Req) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// MiddleCancel wrap a context function as a chi middleware
|
||||
func MiddleCancel(f func(ctx *context.Context) goctx.CancelFunc) func(netx http.Handler) http.Handler { |
||||
funcInfo := routing.GetFuncInfo(f) |
||||
return func(next http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetContext(req) |
||||
cancel := f(ctx) |
||||
if cancel != nil { |
||||
defer cancel() |
||||
} |
||||
if ctx.Written() { |
||||
return |
||||
} |
||||
next.ServeHTTP(ctx.Resp, ctx.Req) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// MiddleAPI wrap a context function as a chi middleware
|
||||
func MiddleAPI(f func(ctx *context.APIContext)) func(next http.Handler) http.Handler { |
||||
funcInfo := routing.GetFuncInfo(f) |
||||
return func(next http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetAPIContext(req) |
||||
f(ctx) |
||||
if ctx.Written() { |
||||
return |
||||
} |
||||
next.ServeHTTP(ctx.Resp, ctx.Req) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
// WrapWithPrefix wraps a provided handler function at a prefix
|
||||
func WrapWithPrefix(pathPrefix string, handler http.HandlerFunc, friendlyName ...string) func(next http.Handler) http.Handler { |
||||
funcInfo := routing.GetFuncInfo(handler, friendlyName...) |
||||
|
||||
return func(next http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
||||
if !strings.HasPrefix(req.URL.Path, pathPrefix) { |
||||
next.ServeHTTP(resp, req) |
||||
return |
||||
} |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
handler(resp, req) |
||||
}) |
||||
} |
||||
} |
@ -1,109 +0,0 @@ |
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package web |
||||
|
||||
import ( |
||||
goctx "context" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"code.gitea.io/gitea/modules/context" |
||||
"code.gitea.io/gitea/modules/web/routing" |
||||
) |
||||
|
||||
type wrappedHandlerFunc func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) |
||||
|
||||
func convertHandler(handler interface{}) wrappedHandlerFunc { |
||||
funcInfo := routing.GetFuncInfo(handler) |
||||
switch t := handler.(type) { |
||||
case http.HandlerFunc: |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
if _, ok := resp.(context.ResponseWriter); !ok { |
||||
resp = context.NewResponse(resp) |
||||
} |
||||
t(resp, req) |
||||
if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { |
||||
done = true |
||||
} |
||||
return done, deferrable |
||||
} |
||||
case func(http.ResponseWriter, *http.Request): |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
t(resp, req) |
||||
if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { |
||||
done = true |
||||
} |
||||
return done, deferrable |
||||
} |
||||
|
||||
case func(ctx *context.Context): |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetContext(req) |
||||
t(ctx) |
||||
done = ctx.Written() |
||||
return done, deferrable |
||||
} |
||||
case func(ctx *context.Context) goctx.CancelFunc: |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetContext(req) |
||||
deferrable = t(ctx) |
||||
done = ctx.Written() |
||||
return done, deferrable |
||||
} |
||||
case func(*context.APIContext): |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetAPIContext(req) |
||||
t(ctx) |
||||
done = ctx.Written() |
||||
return done, deferrable |
||||
} |
||||
case func(*context.APIContext) goctx.CancelFunc: |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetAPIContext(req) |
||||
deferrable = t(ctx) |
||||
done = ctx.Written() |
||||
return done, deferrable |
||||
} |
||||
case func(*context.PrivateContext): |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetPrivateContext(req) |
||||
t(ctx) |
||||
done = ctx.Written() |
||||
return done, deferrable |
||||
} |
||||
case func(*context.PrivateContext) goctx.CancelFunc: |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
ctx := context.GetPrivateContext(req) |
||||
deferrable = t(ctx) |
||||
done = ctx.Written() |
||||
return done, deferrable |
||||
} |
||||
case func(http.Handler) http.Handler: |
||||
return func(resp http.ResponseWriter, req *http.Request, others ...wrappedHandlerFunc) (done bool, deferrable func()) { |
||||
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}) |
||||
if len(others) > 0 { |
||||
next = wrapInternal(others) |
||||
} |
||||
routing.UpdateFuncInfo(req.Context(), funcInfo) |
||||
if _, ok := resp.(context.ResponseWriter); !ok { |
||||
resp = context.NewResponse(resp) |
||||
} |
||||
t(next).ServeHTTP(resp, req) |
||||
if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 { |
||||
done = true |
||||
} |
||||
return done, deferrable |
||||
} |
||||
default: |
||||
panic(fmt.Sprintf("Unsupported handler type: %#v", t)) |
||||
} |
||||
} |
Loading…
Reference in new issue