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 already
2 years ago
|
|
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
package web
|
|
|
|
|
|
|
|
import (
|
|
|
|
goctx "context"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"reflect"
|
|
|
|
|
|
|
|
"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 {
|
|
|
|
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()))
|
|
|
|
}
|
|
|
|
|
|
|
|
if hp, ok := handler.(func(next http.Handler) http.Handler); ok {
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
|
|
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
|
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
|
|
|
h.ServeHTTP(resp, req)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 already
2 years ago
|
|
|
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
|
|
|
|
}
|