mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							
								
								
									
										200
									
								
								modules/web/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								modules/web/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user