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
2023-04-20 21:49:06 +03:00
// 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 ( ) ) )
}
2023-04-27 09:06:45 +03:00
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
2023-04-20 21:49:06 +03:00
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
}