mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
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:
parent
a018ba9b0a
commit
8e049d77c9
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
59
_examples/mvc/error-handler-http/main.go
Normal file
59
_examples/mvc/error-handler-http/main.go
Normal 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
|
||||
}
|
20
_examples/mvc/error-handler-http/main_test.go
Normal file
20
_examples/mvc/error-handler-http/main_test.go
Normal 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)
|
||||
}
|
1
_examples/mvc/error-handler-http/views/404.html
Normal file
1
_examples/mvc/error-handler-http/views/404.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h3>Not Found Custom Page Rendered through Controller's HandleHTTPError</h3>
|
1
_examples/mvc/error-handler-http/views/500.html
Normal file
1
_examples/mvc/error-handler-http/views/500.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h3>Internal Server Err</h3>
|
|
@ -0,0 +1 @@
|
|||
<h1>Unexpected Error</h1>
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`.
|
||||
//
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user