diff --git a/HISTORY.md b/HISTORY.md
index 307e4b65..fd3b914a 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -28,6 +28,18 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
+- New `FallbackView` feature, per-party or per handler chain. Example can be found at: [_examples/view/fallback](_examples/view/fallback).
+
+```go
+ app.FallbackView(iris.FallbackViewFunc(func(ctx iris.Context, err iris.ErrViewNotExist) error {
+ // err.Name is the previous template name.
+ // err.IsLayout reports whether the failure came from the layout template.
+ // err.Data is the template data provided to the previous View call.
+ // [...custom logic e.g. ctx.View("fallback.html", err.Data)]
+ return err
+ }))
+```
+
- New `versioning.Aliases` middleware and up to 80% faster version resolve. Example Code:
```go
diff --git a/_examples/view/fallback/main.go b/_examples/view/fallback/main.go
new file mode 100644
index 00000000..3c722673
--- /dev/null
+++ b/_examples/view/fallback/main.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "github.com/kataras/iris/v12"
+)
+
+const defaultViewName = "fallback"
+
+func main() {
+ app := iris.New()
+ app.RegisterView(iris.HTML("./view", ".html"))
+
+ // Use the FallbackView helper Register a fallback view
+ // filename per-party when the provided was not found.
+ app.FallbackView(iris.FallbackView("fallback.html"))
+
+ // Use the FallbackViewLayout helper to register a fallback view layout.
+ app.FallbackView(iris.FallbackViewLayout("layout.html"))
+
+ // Register a custom fallback function per-party to handle everything.
+ // You can register more than one. If fails (returns a not nil error of ErrViewNotExists)
+ // then it proceeds to the next registered fallback.
+ app.FallbackView(iris.FallbackViewFunc(func(ctx iris.Context, err iris.ErrViewNotExist) error {
+ // err.Name is the previous template name.
+ // err.IsLayout reports whether the failure came from the layout template.
+ // err.Data is the template data provided to the previous View call.
+ // [...custom logic e.g. ctx.View("fallback.html", err.Data)]
+ return err
+ }))
+
+ app.Get("/", index)
+
+ app.Listen(":8080")
+}
+
+// Register fallback view(s) in a middleware.
+// func fallbackInsideAMiddleware(ctx iris.Context) {
+// ctx.FallbackView(...)
+// To remove all previous registered fallbacks, pass nil.
+// ctx.FallbackView(nil)
+// ctx.Next()
+// }
+
+func index(ctx iris.Context) {
+ ctx.View("blabla.html")
+}
diff --git a/_examples/view/fallback/view/fallback.html b/_examples/view/fallback/view/fallback.html
new file mode 100644
index 00000000..c10ca04c
--- /dev/null
+++ b/_examples/view/fallback/view/fallback.html
@@ -0,0 +1 @@
+
Fallback view
\ No newline at end of file
diff --git a/aliases.go b/aliases.go
index bda2c3b5..8d596cca 100644
--- a/aliases.go
+++ b/aliases.go
@@ -252,6 +252,24 @@ var (
Ace = view.Ace
)
+type (
+ // ErrViewNotExist reports whether a template was not found in the parsed templates tree.
+ ErrViewNotExist = context.ErrViewNotExist
+ // FallbackViewFunc is a function that can be registered
+ // to handle view fallbacks. It accepts the Context and
+ // a special error which contains information about the previous template error.
+ // It implements the FallbackViewProvider interface.
+ //
+ // See `Context.View` method.
+ FallbackViewFunc = context.FallbackViewFunc
+ // FallbackView is a helper to register a single template filename as a fallback
+ // when the provided tempate filename was not found.
+ FallbackView = context.FallbackView
+ // FallbackViewLayout is a helper to register a single template filename as a fallback
+ // layout when the provided layout filename was not found.
+ FallbackViewLayout = context.FallbackViewLayout
+)
+
// PrefixDir returns a new FileSystem that opens files
// by adding the given "prefix" to the directory tree of "fs".
//
diff --git a/configuration.go b/configuration.go
index 7e96aac9..17b9a910 100644
--- a/configuration.go
+++ b/configuration.go
@@ -797,6 +797,11 @@ type Configuration struct {
//
// Defaults to "iris.view.data".
ViewDataContextKey string `ini:"view_data_context_key" json:"viewDataContextKey,omitempty" yaml:"ViewDataContextKey" toml:"ViewDataContextKey"`
+ // FallbackViewContextKey is the context's values key
+ // responsible to store the view fallback information.
+ //
+ // Defaults to "iris.view.fallback".
+ FallbackViewContextKey string `ini:"fallback_view_context_key" json:"fallbackViewContextKey,omitempty" yaml:"FallbackViewContextKey" toml:"FallbackViewContextKey"`
// RemoteAddrHeaders are the allowed request headers names
// that can be valid to parse the client's IP based on.
// By-default no "X-" header is consired safe to be used for retrieving the
@@ -999,6 +1004,11 @@ func (c Configuration) GetViewDataContextKey() string {
return c.ViewDataContextKey
}
+// GetFallbackViewContextKey returns the FallbackViewContextKey field.
+func (c Configuration) GetFallbackViewContextKey() string {
+ return c.FallbackViewContextKey
+}
+
// GetRemoteAddrHeaders returns the RemoteAddrHeaders field.
func (c Configuration) GetRemoteAddrHeaders() []string {
return c.RemoteAddrHeaders
@@ -1155,6 +1165,9 @@ func WithConfiguration(c Configuration) Configurator {
if v := c.ViewDataContextKey; v != "" {
main.ViewDataContextKey = v
}
+ if v := c.FallbackViewContextKey; v != "" {
+ main.FallbackViewContextKey = v
+ }
if v := c.RemoteAddrHeaders; len(v) > 0 {
main.RemoteAddrHeaders = v
@@ -1228,6 +1241,7 @@ func DefaultConfiguration() Configuration {
ViewEngineContextKey: "iris.view.engine",
ViewLayoutContextKey: "iris.view.layout",
ViewDataContextKey: "iris.view.data",
+ FallbackViewContextKey: "iris.view.fallback",
RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{
diff --git a/context/configuration.go b/context/configuration.go
index c2dcb6e6..4670666c 100644
--- a/context/configuration.go
+++ b/context/configuration.go
@@ -62,6 +62,8 @@ type ConfigurationReadOnly interface {
GetViewLayoutContextKey() string
// GetViewDataContextKey returns the ViewDataContextKey field.
GetViewDataContextKey() string
+ // GetFallbackViewContextKey returns the FallbackViewContextKey field.
+ GetFallbackViewContextKey() string
// GetRemoteAddrHeaders returns RemoteAddrHeaders field.
GetRemoteAddrHeaders() []string
diff --git a/context/context.go b/context/context.go
index 22cf0ce1..1c4b4ffc 100644
--- a/context/context.go
+++ b/context/context.go
@@ -2971,6 +2971,147 @@ func (ctx *Context) GetViewData() map[string]interface{} {
return nil
}
+// FallbackViewProvider is an interface which can be registered to the `Party.FallbackView`
+// or `Context.FallbackView` methods to handle fallback views.
+// See FallbackView, FallbackViewLayout and FallbackViewFunc.
+type FallbackViewProvider interface {
+ FallbackView(ctx *Context, err ErrViewNotExist) error
+} /* Notes(@kataras): If ever requested, this fallback logic (of ctx, error) can go to all necessary methods.
+ I've designed with a bit more complexity here instead of a simple filename fallback in order to give
+ the freedom to the developer to do whatever he/she wants with that template/layout not exists error,
+ e.g. have a list of fallbacks views to loop through until succeed or fire a different error than the default.
+ We also provide some helpers for common fallback actions (FallbackView, FallbackViewLayout).
+ This naming was chosen in order to be easy to follow up with the previous view-relative context features.
+ Also note that here we catch a specific error, we want the developer
+ to be aware of the rest template errors (e.g. when a template having parsing issues).
+*/
+
+// FallbackViewFunc is a function that can be registered
+// to handle view fallbacks. It accepts the Context and
+// a special error which contains information about the previous template error.
+// It implements the FallbackViewProvider interface.
+//
+// See `Context.View` method.
+type FallbackViewFunc func(ctx *Context, err ErrViewNotExist) error
+
+// FallbackView completes the FallbackViewProvider interface.
+func (fn FallbackViewFunc) FallbackView(ctx *Context, err ErrViewNotExist) error {
+ return fn(ctx, err)
+}
+
+var (
+ _ FallbackViewProvider = FallbackView("")
+ _ FallbackViewProvider = FallbackViewLayout("")
+)
+
+// FallbackView is a helper to register a single template filename as a fallback
+// when the provided tempate filename was not found.
+type FallbackView string
+
+// FallbackView completes the FallbackViewProvider interface.
+func (f FallbackView) FallbackView(ctx *Context, err ErrViewNotExist) error {
+ if err.IsLayout { // Not responsible to render layouts.
+ return err
+ }
+
+ // ctx.StatusCode(200) // Let's keep the previous status code here, developer can change it anyways.
+ return ctx.View(string(f), err.Data)
+}
+
+// FallbackViewLayout is a helper to register a single template filename as a fallback
+// layout when the provided layout filename was not found.
+type FallbackViewLayout string
+
+// FallbackView completes the FallbackViewProvider interface.
+func (f FallbackViewLayout) FallbackView(ctx *Context, err ErrViewNotExist) error {
+ if !err.IsLayout {
+ // Responsible to render layouts only.
+ return err
+ }
+
+ ctx.ViewLayout(string(f))
+ return ctx.View(err.Name, err.Data)
+}
+
+const fallbackViewOnce = "iris.fallback.view.once"
+
+func (ctx *Context) fireFallbackViewOnce(err ErrViewNotExist) error {
+ // Note(@kataras): this is our way to keep the same View method for
+ // both fallback and normal views, remember, we export the whole
+ // Context functionality to the end-developer through the fallback view provider.
+ if ctx.values.Get(fallbackViewOnce) != nil {
+ return err
+ }
+
+ v := ctx.values.Get(ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey())
+ if v == nil {
+ return err
+ }
+
+ providers, ok := v.([]FallbackViewProvider)
+ if !ok {
+ return err
+ }
+
+ ctx.values.Set(fallbackViewOnce, struct{}{})
+
+ var pErr error
+ for _, provider := range providers {
+ pErr = provider.FallbackView(ctx, err)
+ if pErr != nil {
+ if vErr, ok := pErr.(ErrViewNotExist); ok {
+ // This fallback view does not exist or it's not responsible to handle,
+ // try the next.
+ pErr = vErr
+ continue
+ }
+ }
+
+ // If OK then we found the correct fallback.
+ // If the error was a parse error and not a template not found
+ // then exit and report the pErr error.
+ break
+ }
+
+ return pErr
+}
+
+// FallbackView registers one or more fallback views for a template or a template layout.
+// When View cannot find the given filename to execute then this "provider"
+// is responsible to handle the error or render a different view.
+//
+// Usage:
+// FallbackView(iris.FallbackView("fallback.html"))
+// FallbackView(iris.FallbackViewLayout("layouts/fallback.html"))
+// OR
+// FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error {
+// err.Name is the previous template name.
+// err.IsLayout reports whether the failure came from the layout template.
+// err.Data is the template data provided to the previous View call.
+// [...custom logic e.g. ctx.View("fallback", err.Data)]
+// })
+func (ctx *Context) FallbackView(providers ...FallbackViewProvider) {
+ key := ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey()
+ if key == "" {
+ return
+ }
+
+ v := ctx.values.Get(key)
+ if v == nil {
+ ctx.values.Set(key, providers)
+ return
+ }
+
+ // Can register more than one.
+ storedProviders, ok := v.([]FallbackViewProvider)
+ if !ok {
+ return
+ }
+
+ storedProviders = append(storedProviders, providers...)
+ ctx.values.Set(key, storedProviders)
+}
+
// View renders a template based on the registered view engine(s).
// First argument accepts the filename, relative to the view engine's Directory and Extension,
// i.e: if directory is "./templates" and want to render the "./templates/users/index.html"
@@ -2985,8 +3126,26 @@ func (ctx *Context) GetViewData() map[string]interface{} {
// Examples: https://github.com/kataras/iris/tree/master/_examples/view
func (ctx *Context) View(filename string, optionalViewModel ...interface{}) error {
ctx.ContentType(ContentHTMLHeaderValue)
- cfg := ctx.app.ConfigurationReadOnly()
+ err := ctx.renderView(filename, optionalViewModel...)
+ if errNotExists, ok := err.(ErrViewNotExist); ok {
+ err = ctx.fireFallbackViewOnce(errNotExists)
+ }
+
+ if err != nil {
+ if ctx.app.Logger().Level == golog.DebugLevel {
+ // send the error back to the client, when debug mode.
+ ctx.StopWithError(http.StatusInternalServerError, err)
+ } else {
+ ctx.StopWithStatus(http.StatusInternalServerError)
+ }
+ }
+
+ return err
+}
+
+func (ctx *Context) renderView(filename string, optionalViewModel ...interface{}) error {
+ cfg := ctx.app.ConfigurationReadOnly()
layout := ctx.values.GetString(cfg.GetViewLayoutContextKey())
var bindingData interface{}
@@ -3000,28 +3159,12 @@ func (ctx *Context) View(filename string, optionalViewModel ...interface{}) erro
if key := cfg.GetViewEngineContextKey(); key != "" {
if engineV := ctx.values.Get(key); engineV != nil {
if engine, ok := engineV.(ViewEngine); ok {
- err := engine.ExecuteWriter(ctx, filename, layout, bindingData)
- if err != nil {
- ctx.app.Logger().Errorf("View [%v] [%T]: %v", ctx.getLogIdentifier(), engine, err)
- return err
- }
-
- return nil
+ return engine.ExecuteWriter(ctx, filename, layout, bindingData)
}
}
}
- err := ctx.app.View(ctx, filename, layout, bindingData) // if failed it logs the error.
- if err != nil {
- if ctx.app.Logger().Level == golog.DebugLevel {
- // send the error back to the client, when debug mode.
- ctx.StopWithError(http.StatusInternalServerError, err)
- } else {
- ctx.StopWithStatus(http.StatusInternalServerError)
- }
- }
-
- return err
+ return ctx.app.View(ctx, filename, layout, bindingData)
}
// getLogIdentifier returns the ID, or the client remote IP address,
diff --git a/context/view.go b/context/view.go
index 35ac1788..dcf1bced 100644
--- a/context/view.go
+++ b/context/view.go
@@ -1,6 +1,26 @@
package context
-import "io"
+import (
+ "fmt"
+ "io"
+)
+
+// ErrViewNotExist it's an error.
+// It reports whether a template was not found in the parsed templates tree.
+type ErrViewNotExist struct {
+ Name string
+ IsLayout bool
+ Data interface{}
+}
+
+// Error completes the `error` interface.
+func (e ErrViewNotExist) Error() string {
+ title := "template"
+ if e.IsLayout {
+ title = "layout"
+ }
+ return fmt.Sprintf("%s '%s' does not exist", title, e.Name)
+}
// ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris.
type ViewEngine interface {
diff --git a/core/router/api_builder.go b/core/router/api_builder.go
index f29232dc..b960d91b 100644
--- a/core/router/api_builder.go
+++ b/core/router/api_builder.go
@@ -1491,6 +1491,26 @@ func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) {
// to keep the iris.Application a compatible Party.
}
+// FallbackView registers one or more fallback views for a template or a template layout.
+// Usage:
+// FallbackView(iris.FallbackView("fallback.html"))
+// FallbackView(iris.FallbackViewLayout("layouts/fallback.html"))
+// OR
+// FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error {
+// err.Name is the previous template name.
+// err.IsLayout reports whether the failure came from the layout template.
+// err.Data is the template data provided to the previous View call.
+// [...custom logic e.g. ctx.View("fallback", err.Data)]
+// })
+func (api *APIBuilder) FallbackView(provider context.FallbackViewProvider) {
+ handler := func(ctx *context.Context) {
+ ctx.FallbackView(provider)
+ ctx.Next()
+ }
+ api.Use(handler)
+ api.UseError(handler)
+}
+
// Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party.
//
diff --git a/core/router/party.go b/core/router/party.go
index 06fe1f66..495e8f19 100644
--- a/core/router/party.go
+++ b/core/router/party.go
@@ -312,6 +312,18 @@ type Party interface {
// To register a view engine per handler chain see the `Context.ViewEngine` instead.
// Read `Configuration.ViewEngineContextKey` documentation for more.
RegisterView(viewEngine context.ViewEngine)
+ // FallbackView registers one or more fallback views for a template or a template layout.
+ // Usage:
+ // FallbackView(iris.FallbackView("fallback.html"))
+ // FallbackView(iris.FallbackViewLayout("layouts/fallback.html"))
+ // OR
+ // FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error {
+ // err.Name is the previous template name.
+ // err.IsLayout reports whether the failure came from the layout template.
+ // err.Data is the template data provided to the previous View call.
+ // [...custom logic e.g. ctx.View("fallback", err.Data)]
+ // })
+ FallbackView(provider context.FallbackViewProvider)
// Layout overrides the parent template layout with a more specific layout for this Party.
// It returns the current Party.
//
diff --git a/iris.go b/iris.go
index aa4d6bbc..d5a5e714 100644
--- a/iris.go
+++ b/iris.go
@@ -410,11 +410,7 @@ func (app *Application) View(writer io.Writer, filename string, layout string, b
return err
}
- err := app.view.ExecuteWriter(writer, filename, layout, bindingData)
- if err != nil {
- app.logger.Error(err)
- }
- return err
+ return app.view.ExecuteWriter(writer, filename, layout, bindingData)
}
// ConfigureHost accepts one or more `host#Configuration`, these configurators functions
diff --git a/view/amber.go b/view/amber.go
index 5e1f2b21..1bda5e1a 100644
--- a/view/amber.go
+++ b/view/amber.go
@@ -217,7 +217,7 @@ func (s *AmberEngine) executeTemplateBuf(name string, binding interface{}) (stri
tmpl := s.fromCache(name)
if tmpl == nil {
s.bufPool.Put(buf)
- return "", ErrNotExist{name, false}
+ return "", ErrNotExist{name, false, binding}
}
err := tmpl.ExecuteTemplate(buf, name, binding)
@@ -253,5 +253,5 @@ func (s *AmberEngine) ExecuteWriter(w io.Writer, filename string, layout string,
return tmpl.Execute(w, bindingData)
}
- return ErrNotExist{filename, false}
+ return ErrNotExist{filename, false, bindingData}
}
diff --git a/view/django.go b/view/django.go
index c9427786..8d6b7d6a 100644
--- a/view/django.go
+++ b/view/django.go
@@ -307,5 +307,5 @@ func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, _ string, bin
return tmpl.ExecuteWriter(getPongoContext(bindingData), w)
}
- return ErrNotExist{filename, false}
+ return ErrNotExist{filename, false, bindingData}
}
diff --git a/view/handlebars.go b/view/handlebars.go
index dd82cb49..a226f21c 100644
--- a/view/handlebars.go
+++ b/view/handlebars.go
@@ -235,5 +235,5 @@ func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout st
return err
}
- return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false}
+ return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false, bindingData}
}
diff --git a/view/html.go b/view/html.go
index 94116ca3..151b9990 100644
--- a/view/html.go
+++ b/view/html.go
@@ -421,14 +421,14 @@ func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bind
t := s.Templates.Lookup(name)
if t == nil {
- return ErrNotExist{name, false}
+ return ErrNotExist{name, false, bindingData}
}
s.runtimeFuncsFor(t, name, bindingData)
if layout = getLayout(layout, s.layout); layout != "" {
lt := s.Templates.Lookup(layout)
if lt == nil {
- return ErrNotExist{layout, true}
+ return ErrNotExist{layout, true, bindingData}
}
s.layoutFuncsFor(lt, name, bindingData)
diff --git a/view/view.go b/view/view.go
index 24b33c15..86acf3b8 100644
--- a/view/view.go
+++ b/view/view.go
@@ -21,19 +21,7 @@ type (
)
// ErrNotExist reports whether a template was not found in the parsed templates tree.
-type ErrNotExist struct {
- Name string
- IsLayout bool
-}
-
-// Error implements the `error` interface.
-func (e ErrNotExist) Error() string {
- title := "template"
- if e.IsLayout {
- title = "layout"
- }
- return fmt.Sprintf("%s '%s' does not exist", title, e.Name)
-}
+type ErrNotExist = context.ErrViewNotExist
// View is just a wrapper on top of the registered template engine.
type View struct{ Engine }