diff --git a/HISTORY.md b/HISTORY.md index 38f89dd9..2ac3199e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -155,6 +155,7 @@ Prior to this version the `iris.Context` was the only one dependency that has be | [time.Time](https://golang.org/pkg/time/#Time) | `time.Now()` | | [*golog.Logger](https://pkg.go.dev/github.com/kataras/golog) | Iris Logger | | [net.IP](https://golang.org/pkg/net/#IP) | `net.ParseIP(ctx.RemoteAddr())` | +| [mvc.Code](https://pkg.go.dev/github.com/kataras/iris/v12/mvc?tab=doc#Code) | `ctx.GetStatusCode()` | | `string`, | | | `int, int8, int16, int32, int64`, | | | `uint, uint8, uint16, uint32, uint64`, | | @@ -362,6 +363,10 @@ Response: Other Improvements: +- New `Controller.HandleHTTPError(mvc.Code) ` optional Controller method to handle http errors as requested at: [MVC - More Elegent OnErrorCode registration?](https://github.com/kataras/iris/issues/1595). Example can be found [here](https://github.com/kataras/iris/tree/master/_examples/mvc/error-handler-http/main.go). + +![MVC: HTTP Error Handler Method](https://user-images.githubusercontent.com/22900943/90948989-e04cd300-e44c-11ea-8c97-54d90fb0cbb6.png) + - New [Rewrite Engine Middleware](https://github.com/kataras/iris/tree/master/middleware/rewrite). Set up redirection rules for path patterns using the syntax we all know. [Example Code](https://github.com/kataras/iris/tree/master/_examples/routing/rewrite). ```yml diff --git a/_examples/README.md b/_examples/README.md index 19935ca0..6cdadd66 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -216,6 +216,7 @@ * [Login (Repository and Service layers)](mvc/login) * [Login (Single Responsibility)](mvc/login-mvc-single-responsibility) * [Vue.js Todo App](mvc/vuejs-todo-mvc) + * [HTTP Error Handler](mvc/error-handler-http) * [Error Handler](mvc/error-handler) * [Handle errors using mvc.Result](mvc/error-handler-custom-result) * [Handle errors using PreflightResult](mvc/error-handler-preflight) diff --git a/_examples/mvc/error-handler-http/main.go b/_examples/mvc/error-handler-http/main.go new file mode 100644 index 00000000..ff148a58 --- /dev/null +++ b/_examples/mvc/error-handler-http/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/mvc" +) + +func main() { + app := newApp() + app.Logger().SetLevel("debug") + app.Listen(":8080") +} + +func newApp() *iris.Application { + app := iris.New() + app.RegisterView(iris.HTML("./views", ".html")) + + m := mvc.New(app) + m.Handle(new(controller)) + + return app +} + +type controller struct{} + +func (c *controller) Get() string { + return "Hello!" +} + +// The input parameter of mvc.Code is optional but a good practise to follow. +// You could register a Context and get its error code through ctx.GetStatusCode(). +// +// This can accept dependencies and output values like any other Controller Method, +// however be careful if your registered dependencies depend only on succesful(200...) requests. +// +// Also note that, if you register more than one controller.HandleHTTPError +// in the same Party, you need to use the RouteOverlap feature as shown +// in the "authenticated-controller" example, and a dependency on +// a controller's field (or method's input argument) is required +// to select which, between those two controllers, is responsible +// to handle http errors. +func (c *controller) HandleHTTPError(statusCode mvc.Code) mvc.View { + code := int(statusCode) // cast it to int. + + view := mvc.View{ + Code: code, + Name: "unexpected-error.html", + } + + switch code { + case 404: + view.Name = "404.html" + // [...] + case 500: + view.Name = "500.html" + } + + return view +} diff --git a/_examples/mvc/error-handler-http/main_test.go b/_examples/mvc/error-handler-http/main_test.go new file mode 100644 index 00000000..7a101091 --- /dev/null +++ b/_examples/mvc/error-handler-http/main_test.go @@ -0,0 +1,20 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/v12/httptest" +) + +func TestControllerHandleHTTPError(t *testing.T) { + const ( + expectedIndex = "Hello!" + expectedNotFound = "

Not Found Custom Page Rendered through Controller's HandleHTTPError

" + ) + + app := newApp() + + e := httptest.New(t, app) + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal(expectedIndex) + e.GET("/a_notefound").Expect().Status(httptest.StatusNotFound).ContentType("text/html").Body().Equal(expectedNotFound) +} diff --git a/_examples/mvc/error-handler-http/views/404.html b/_examples/mvc/error-handler-http/views/404.html new file mode 100644 index 00000000..6175671a --- /dev/null +++ b/_examples/mvc/error-handler-http/views/404.html @@ -0,0 +1 @@ +

Not Found Custom Page Rendered through Controller's HandleHTTPError

\ No newline at end of file diff --git a/_examples/mvc/error-handler-http/views/500.html b/_examples/mvc/error-handler-http/views/500.html new file mode 100644 index 00000000..4228c29d --- /dev/null +++ b/_examples/mvc/error-handler-http/views/500.html @@ -0,0 +1 @@ +

Internal Server Err

\ No newline at end of file diff --git a/_examples/mvc/error-handler-http/views/unexpected-error.html b/_examples/mvc/error-handler-http/views/unexpected-error.html new file mode 100644 index 00000000..5da6be52 --- /dev/null +++ b/_examples/mvc/error-handler-http/views/unexpected-error.html @@ -0,0 +1 @@ +

Unexpected Error

\ No newline at end of file diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 3a787dc4..4509ffaa 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -370,7 +370,7 @@ func (api *APIBuilder) SetRegisterRule(rule RouteRegisterRule) Party { return api } -// Handle registers a route to the server's api. +// Handle registers a route to this Party. // if empty method is passed then handler(s) are being registered to all methods, same as .Any. // // Returns a *Route, app will throw any errors later on. @@ -378,6 +378,8 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co return api.handle(0, method, relativePath, handlers...) } +// handle registers a full route to this Party. +// Use Handle or Get, Post, Put, Delete and et.c. instead. func (api *APIBuilder) handle(errorCode int, method string, relativePath string, handlers ...context.Handler) *Route { routes := api.createRoutes(errorCode, []string{method}, relativePath, handlers...) @@ -1242,6 +1244,14 @@ func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) (routes []*Ro routes = append(routes, api.OnErrorCode(statusCode, handlers...)...) } + if n := len(routes); n > 1 { + for _, r := range routes[1:n] { + r.NoLog = true + } + + routes[0].Title = "ERR" + } + return } diff --git a/core/router/handler.go b/core/router/handler.go index 24683a69..c034a225 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -189,7 +189,13 @@ func (h *routerHandler) Build(provider RoutesProvider) error { return lsub1 > lsub2 }) + noLogCount := 0 + for _, r := range registeredRoutes { + if r.NoLog { + noLogCount++ + } + if h.config != nil && h.config.GetForceLowercaseRouting() { // only in that state, keep everything else as end-developer registered. r.Path = strings.ToLower(r.Path) @@ -225,8 +231,12 @@ func (h *routerHandler) Build(provider RoutesProvider) error { // the route logs are colorful. // Note: don't use map, we need to keep registered order, use // different slices for each method. + collect := func(method string) (methodRoutes []*Route) { for _, r := range registeredRoutes { + if r.NoLog { + continue + } if r.Method == method { methodRoutes = append(methodRoutes, r) } @@ -263,7 +273,7 @@ func (h *routerHandler) Build(provider RoutesProvider) error { // logger.Debugf("API: %d registered %s (", len(registeredRoutes), tr) // with: pio.WriteRich(logger.Printer, debugLevel.Title, debugLevel.ColorCode, debugLevel.Style...) - fmt.Fprintf(logger.Printer, " %s %sAPI: %d registered %s (", time.Now().Format(logger.TimeFormat), logger.Prefix, len(registeredRoutes), tr) + fmt.Fprintf(logger.Printer, " %s %sAPI: %d registered %s (", time.Now().Format(logger.TimeFormat), logger.Prefix, len(registeredRoutes)-noLogCount, tr) // logger.NewLine = bckpNewLine diff --git a/core/router/route.go b/core/router/route.go index 29c51dbc..15a89542 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -19,6 +19,7 @@ import ( // If any of the following fields are changed then the // caller should Refresh the router. type Route struct { + Title string `json"title"` // custom name to replace the method on debug logging. Name string `json:"name"` // "userRoute" Description string `json:"description"` // "lists a user" Method string `json:"method"` // "GET" @@ -63,6 +64,7 @@ type Route struct { // OnBuild runs right before BuildHandlers. OnBuild func(r *Route) + NoLog bool // disables debug logging. } // NewRoute returns a new route based on its method, @@ -349,14 +351,14 @@ func (r *Route) ResolvePath(args ...string) string { return formattedPath } -func traceHandlerFile(method, name, line string, number int) string { +func traceHandlerFile(title, name, line string, number int) string { file := fmt.Sprintf("(%s:%d)", filepath.ToSlash(line), number) if context.IgnoreHandlerName(name) { return "" } - space := strings.Repeat(" ", len(method)+1) + space := strings.Repeat(" ", len(title)+1) return fmt.Sprintf("\n%s • %s %s", space, name, file) } @@ -389,18 +391,22 @@ func traceMethodColor(method string) int { // * @second_handler ... // If route and handler line:number locations are equal then the second is ignored. func (r *Route) Trace(w io.Writer, stoppedIndex int) { - method := r.Method - if method == "" { - method = fmt.Sprintf("%d", r.StatusCode) + title := r.Title + if title == "" { + if r.StatusCode > 0 { + title = fmt.Sprintf("%d", r.StatusCode) // if error code then title is the status code, e.g. 400. + } else { + title = r.Method // else is its method, e.g. GET + } } // Color the method. - color := traceMethodColor(method) + color := traceMethodColor(title) // @method: @path // space := strings.Repeat(" ", len(http.MethodConnect)-len(method)) - // s := fmt.Sprintf("%s: %s", pio.Rich(method, color), path) - pio.WriteRich(w, method, color) + // s := fmt.Sprintf("%s: %s", pio.Rich(title, color), path) + pio.WriteRich(w, title, color) path := r.Tmpl().Src if path == "" { @@ -412,7 +418,7 @@ func (r *Route) Trace(w io.Writer, stoppedIndex int) { // (@description) description := r.Description if description == "" { - if method == MethodNone { + if title == MethodNone { description = "offline" } @@ -469,7 +475,7 @@ func (r *Route) Trace(w io.Writer, stoppedIndex int) { } // * @handler_name (@handler_rel_location) - fmt.Fprint(w, traceHandlerFile(r.Method, name, file, line)) + fmt.Fprint(w, traceHandlerFile(title, name, file, line)) if stoppedIndex != -1 && stoppedIndex <= len(r.Handlers) { if i <= stoppedIndex { pio.WriteRich(w, " ✓", pio.Green) diff --git a/hero/container.go b/hero/container.go index b82cebcd..7471fd5b 100644 --- a/hero/container.go +++ b/hero/container.go @@ -84,6 +84,10 @@ var BuiltinDependencies = []*Dependency{ NewDependency(func(ctx *context.Context) net.IP { return net.ParseIP(ctx.RemoteAddr()) }).Explicitly(), + // Status Code (special type for MVC HTTP Error handler to not conflict with path parameters) + NewDependency(func(ctx *context.Context) Code { + return Code(ctx.GetStatusCode()) + }).Explicitly(), // payload and param bindings are dynamically allocated and declared at the end of the `binding` source file. } diff --git a/hero/handler.go b/hero/handler.go index 673a8ece..3baa8229 100644 --- a/hero/handler.go +++ b/hero/handler.go @@ -18,6 +18,9 @@ type ( // ErrorHandlerFunc implements the `ErrorHandler`. // It describes the type defnition for an error function handler. ErrorHandlerFunc func(*context.Context, error) + + // Code is a special type for status code. + Code int ) // HandleError fires when a non-nil error returns from a request-scoped dependency at serve-time or the handler itself. diff --git a/hero/struct.go b/hero/struct.go index 2ee16f77..7d537164 100644 --- a/hero/struct.go +++ b/hero/struct.go @@ -88,9 +88,7 @@ func makeStruct(structPtr interface{}, c *Container, partyParamsCount int) *Stru newContainer := c.Clone() // Add the controller dependency itself as func dependency but with a known type which should be explicit binding // in order to keep its maximum priority. - newContainer.Register(s.Acquire). - Explicitly(). - DestType = typ + newContainer.Register(s.Acquire).Explicitly().DestType = typ newContainer.GetErrorHandler = func(ctx *context.Context) ErrorHandler { if isErrHandler { diff --git a/mvc/aliases.go b/mvc/aliases.go index b8f87c86..f52b09d6 100644 --- a/mvc/aliases.go +++ b/mvc/aliases.go @@ -12,6 +12,10 @@ type ( Response = hero.Response // View is a type alias for the `hero#View`, useful for output controller's methods. View = hero.View + // Code is a type alias for the `hero#Code`, useful for + // http error handling in controllers. + // This can be one of the input parameters of the `Controller.HandleHTTPError`. + Code = hero.Code // DeprecationOptions describes the deprecation headers key-values. // Is a type alias for the `versioning#DeprecationOptions`. // diff --git a/mvc/controller.go b/mvc/controller.go index 939a6dcb..d21e1bd0 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -135,8 +135,14 @@ func newControllerActivator(app *Application, controller interface{}) *Controlle return c } +// It's a dynamic method, can be exist or not, it can accept input arguments +// and can write through output values like any other dev-designed method. +// See 'parseHTTPErrorMethod'. +// Example at: _examples/mvc/error-handler-http +const handleHTTPErrorMethodName = "HandleHTTPError" + func whatReservedMethods(typ reflect.Type) map[string][]*router.Route { - methods := []string{"BeforeActivation", "AfterActivation"} + methods := []string{"BeforeActivation", "AfterActivation", handleHTTPErrorMethodName} // BeforeActivatior/AfterActivation are not routes but they are // reserved names* if isBaseController(typ) { @@ -287,9 +293,16 @@ func (c *ControllerActivator) activate() { return } + c.parseHTTPErrorHandler() c.parseMethods() } +func (c *ControllerActivator) parseHTTPErrorHandler() { + if m, ok := c.Type.MethodByName(handleHTTPErrorMethodName); ok { + c.handleHTTPError(m.Name) + } +} + // register all available, exported methods to handlers if possible. func (c *ControllerActivator) parseMethods() { n := c.Type.NumMethod() @@ -334,6 +347,40 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . return routes[0] } +// handleHTTPError is called when a controller's method +// with the "HandleHTTPError" is found. That method +// can accept dependencies like the rest but if it's not called manually +// then any dynamic dependencies depending on succesful requests +// may fail - this is end-developer's job; +// to register the correct dependencies or not do it all on that method. +// +// Note that if more than one controller in the same Party +// tries to register an http error handler then the +// overlap route rule should be used and a dependency +// on the controller (or method) level that will select +// between the two should exist (see mvc/authenticated-controller example). +func (c *ControllerActivator) handleHTTPError(funcName string) *router.Route { + handler := c.handlerOf("/", funcName) + + routes := c.app.Router.OnAnyErrorCode(handler) + if len(routes) == 0 { + err := fmt.Errorf("MVC: unable to register an HTTP error code handler for '%s.%s'", c.fullName, funcName) + c.addErr(err) + return nil + } + + for _, r := range routes { + r.Description = "controller" + r.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) + if m, ok := c.Type.MethodByName(funcName); ok { + r.SourceFileName, r.SourceLineNumber = context.HandlerFileLineRel(m.Func) + } + } + + c.routes[funcName] = routes + return routes[0] +} + // HandleMany like `Handle` but can register more than one path and HTTP method routes // separated by whitespace on the same controller's method. // Keep note that if the controller's method input arguments are path parameters dependencies