HandleHTTPError MVC Method as requested at #1595. Read HISTORY.md

example at: https://github.com/kataras/iris/tree/master/_examples/mvc/error-handler-http
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-08-22 08:04:22 +03:00
parent a018ba9b0a
commit 8e049d77c9
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
15 changed files with 186 additions and 16 deletions

View File

@ -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) <T>` 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

View File

@ -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)

View File

@ -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
}

View File

@ -0,0 +1,20 @@
package main
import (
"testing"
"github.com/kataras/iris/v12/httptest"
)
func TestControllerHandleHTTPError(t *testing.T) {
const (
expectedIndex = "Hello!"
expectedNotFound = "<h3>Not Found Custom Page Rendered through Controller's HandleHTTPError</h3>"
)
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)
}

View File

@ -0,0 +1 @@
<h3>Not Found Custom Page Rendered through Controller's HandleHTTPError</h3>

View File

@ -0,0 +1 @@
<h3>Internal Server Err</h3>

View File

@ -0,0 +1 @@
<h1>Unexpected Error</h1>

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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.
}

View File

@ -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.

View File

@ -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 {

View File

@ -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`.
//

View File

@ -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