diff --git a/HISTORY.md b/HISTORY.md index 9e57c7db..030f78ce 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -15,9 +15,83 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready. > Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes. -**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. -For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework). +**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. +# Th, 13 July 2017 | v8.0.1 + +Nothing tremendous at this minor version. + +We've just added a configuration field in order to ignore errors received by the `Run` function, see below. + +[Configuration#IgnoreServerErrors](https://github.com/kataras/iris/blob/master/configuration.go#L255) +```go +type Configuration struct { + // [...] + + // IgnoreServerErrors will cause to ignore the matched "errors" + // from the main application's `Run` function. + // This is a slice of string, not a slice of error + // users can register these errors using yaml or toml configuration file + // like the rest of the configuration fields. + // + // See `WithoutServerError(...)` function too. + // + // Defaults to an empty slice. + IgnoreServerErrors []string `yaml:"IgnoreServerErrors" toml:"IgnoreServerErrors"` + + // [...] +} +``` +[Configuration#WithoutServerError](https://github.com/kataras/iris/blob/master/configuration.go#L106) +```go +// WithoutServerError will cause to ignore the matched "errors" +// from the main application's `Run` function. +// +// Usage: +// err := app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) +// will return `nil` if the server's error was `http/iris#ErrServerClosed`. +// +// See `Configuration#IgnoreServerErrors []string` too. +WithoutServerError(errors ...error) Configurator +``` + +By default no error is being ignored, of course. + +Example code: +[_examples/http-listening/omit-server-errors](https://github.com/kataras/iris/tree/master/_examples/http-listening/omit-server-errors) +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/context" +) + +func main() { + app := iris.New() + + app.Get("/", func(ctx context.Context) { + ctx.HTML("

Hello World!/

") + }) + + err := app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) + if err != nil { + // do something + } + // same as: + // err := app.Run(iris.Addr(":8080")) + // if err != nil && (err != iris.ErrServerClosed || err.Error() != iris.ErrServerClosed.Error()) { + // [...] + // } +} +``` + +At first we didn't want to implement something like that because it's ridiculous easy to do it manually but a second thought came to us, +that many applications are based on configuration, therefore it would be nice to have something to ignore errors +by simply string values that can be passed to the application's configuration via `toml` or `yaml` files too. + +This feature has been implemented after a request of ignoring the `iris/http#ErrServerClosed` from the `Run` function: +https://github.com/kataras/iris/issues/668 # Mo, 10 July 2017 | v8.0.0 diff --git a/README.md b/README.md index 213d0465..a19c9793 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ These types of projects need heart and sacrifices to continue offer the best dev * [Installation](#-installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#mo-10-july-2017--v800) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#th-13-july-2017--v801) * [Learn](#-learn) * [HTTP Listening](_examples/#http-listening) * [Configuration](_examples/#configuration) @@ -215,7 +215,7 @@ _iris_ does not force you to use any specific ORM. With support for the most pop ### 📌 Version -Current: **8.0.0** +Current: **8.0.1** Each new release is pushed to the master. It stays there until the next version. When a next version is released then the previous version goes to its own branch with `gopkg.in` as its import path (and its own vendor folder), in order to keep it working "for-ever". diff --git a/_examples/README.md b/_examples/README.md index b9ab6a34..63d5633d 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -16,6 +16,7 @@ It doesn't always contain the "best ways" but it does cover each important featu ### HTTP Listening - [Common, with address](http-listening/listen-addr/main.go) + * [omit server errors](http-listening/listen-addr/omit-server-errors) - [UNIX socket file](http-listening/listen-unix/main.go) - [TLS](http-listening/listen-tls/main.go) - [Letsencrypt (Automatic Certifications)](http-listening/listen-letsencrypt/main.go) diff --git a/_examples/hello-world/main_go19.go b/_examples/hello-world/main_go19.go deleted file mode 100644 index 6a35f5cd..00000000 --- a/_examples/hello-world/main_go19.go +++ /dev/null @@ -1,35 +0,0 @@ -// +build go1.9 - -package main - -import ( - "github.com/kataras/iris" -) - -func main() { - app := iris.Default() - - // Method: GET - // Resource: http://localhost:8080/ - app.Handle("GET", "/", func(ctx iris.Context) { - ctx.HTML("Hello world!") - }) - - // same as app.Handle("GET", "/ping", [...]) - // Method: GET - // Resource: http://localhost:8080/ping - app.Get("/ping", func(ctx iris.Context) { - ctx.WriteString("pong") - }) - - // Method: GET - // Resource: http://localhost:8080/hello - app.Get("/hello", func(ctx iris.Context) { - ctx.JSON(iris.Map{"message": "Hello iris web framework."}) - }) - - // http://localhost:8080 - // http://localhost:8080/ping - // http://localhost:8080/hello - app.Run(iris.Addr(":8080")) -} diff --git a/_examples/http-listening/listen-addr/main.go b/_examples/http-listening/listen-addr/main.go index 673057ca..d89c7a3f 100644 --- a/_examples/http-listening/listen-addr/main.go +++ b/_examples/http-listening/listen-addr/main.go @@ -12,8 +12,6 @@ func main() { ctx.HTML("

Hello World!/

") }) - if err := app.Run(iris.Addr(":8080")); err != nil { - panic(err) - } - + // http://localhost:8080 + app.Run(iris.Addr(":8080")) } diff --git a/_examples/http-listening/listen-addr/omit-server-errors/main.go b/_examples/http-listening/listen-addr/omit-server-errors/main.go new file mode 100644 index 00000000..7b7a5c53 --- /dev/null +++ b/_examples/http-listening/listen-addr/omit-server-errors/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/context" +) + +func main() { + app := iris.New() + + app.Get("/", func(ctx context.Context) { + ctx.HTML("

Hello World!/

") + }) + + err := app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) + if err != nil { + // do something + } + // same as: + // err := app.Run(iris.Addr(":8080")) + // if err != nil && (err != iris.ErrServerClosed || err.Error() != iris.ErrServerClosed.Error()) { + // [...] + // } +} diff --git a/_examples/http-listening/listen-addr/omit-server-errors/main_test.go b/_examples/http-listening/listen-addr/omit-server-errors/main_test.go new file mode 100644 index 00000000..3413efec --- /dev/null +++ b/_examples/http-listening/listen-addr/omit-server-errors/main_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + stdContext "context" + "fmt" + "testing" + "time" + + "github.com/kataras/iris" + "github.com/sirupsen/logrus" +) + +func logger(app *iris.Application) *bytes.Buffer { + buf := &bytes.Buffer{} + app.Logger().Formatter = &logrus.TextFormatter{ + DisableColors: true, + DisableSorting: true, + DisableTimestamp: true, + } + + app.Logger().Out = buf + + // disable the "Now running at...." in order to have a clean log of the error. + // we could attach that on `Run` but better to keep things simple here. + app.Configure(iris.WithoutStartupLog) + return buf +} + +func TestListenAddr(t *testing.T) { + app := iris.New() + // we keep the logger running as well but in a controlled way. + log := logger(app) + + // close the server at 3-6 seconds + go func() { + time.Sleep(3 * time.Second) + ctx, cancel := stdContext.WithTimeout(stdContext.TODO(), 3*time.Second) + defer cancel() + app.Shutdown(ctx) + }() + + err := app.Run(iris.Addr(":9829")) + // in this case the error should be logged and return as well. + if err != iris.ErrServerClosed { + t.Fatalf("expecting err to be `iris.ErrServerClosed` but got: %v", err) + } + + // println(log.Bytes()) + // println(len(log.Bytes())) + expected := fmt.Sprintln("level=error msg=\"" + iris.ErrServerClosed.Error() + "\" ") + // println([]byte(expected)) + // println(len([]byte(expected))) + + if got := log.String(); expected != got { + t.Fatalf("expecting to log the:\n'%s'\ninstead of:\n'%s'", expected, got) + } +} + +func TestListenAddrWithoutServerErr(t *testing.T) { + app := iris.New() + // we keep the logger running as well but in a controlled way. + log := logger(app) + + // close the server at 3-6 seconds + go func() { + time.Sleep(3 * time.Second) + ctx, cancel := stdContext.WithTimeout(stdContext.TODO(), 3*time.Second) + defer cancel() + app.Shutdown(ctx) + }() + + // we disable the ErrServerClosed, so the error should be nil when server is closed by `app.Shutdown`. + + // so in this case the iris/http.ErrServerClosed should be NOT logged and NOT return. + err := app.Run(iris.Addr(":9827"), iris.WithoutServerError(iris.ErrServerClosed)) + if err != nil { + t.Fatalf("expecting err to be nil but got: %v", err) + } + + if got := log.String(); got != "" { + t.Fatalf("expecting to log nothing but logged: '%s'", got) + } +} diff --git a/configuration.go b/configuration.go index b87990b7..a69d2c0f 100644 --- a/configuration.go +++ b/configuration.go @@ -95,6 +95,29 @@ type Configurator func(*Application) // variables for configurators don't need any receivers, functions // for them that need (helps code editors to recognise as variables without parenthesis completion). +// WithoutServerError will cause to ignore the matched "errors" +// from the main application's `Run` function. +// +// Usage: +// err := app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) +// will return `nil` if the server's error was `http/iris#ErrServerClosed`. +// +// See `Configuration#IgnoreServerErrors []string` too. +func WithoutServerError(errors ...error) Configurator { + return func(app *Application) { + if len(errors) == 0 { + return + } + + errorsAsString := make([]string, len(errors), len(errors)) + for i, e := range errors { + errorsAsString[i] = e.Error() + } + + app.config.IgnoreServerErrors = append(app.config.IgnoreServerErrors, errorsAsString...) + } +} + // WithoutStartupLog turns off the information send, once, to the terminal when the main server is open. var WithoutStartupLog = func(app *Application) { app.config.DisableStartupLog = true @@ -220,6 +243,17 @@ type Configuration struct { // It can be retrieved by the context if needed (i.e router for subdomains) vhost string + // IgnoreServerErrors will cause to ignore the matched "errors" + // from the main application's `Run` function. + // This is a slice of string, not a slice of error + // users can register these errors using yaml or toml configuration file + // like the rest of the configuration fields. + // + // See `WithoutServerError(...)` function too. + // + // Defaults to an empty slice. + IgnoreServerErrors []string `yaml:"IgnoreServerErrors" toml:"IgnoreServerErrors"` + // DisableStartupLog if setted to true then it turns off the write banner on server startup. // // Defaults to false. @@ -455,6 +489,10 @@ func WithConfiguration(c Configuration) Configurator { return func(app *Application) { main := app.config + if v := c.IgnoreServerErrors; len(v) > 0 { + main.IgnoreServerErrors = append(main.IgnoreServerErrors, v...) + } + if v := c.DisableStartupLog; v { main.DisableStartupLog = v } diff --git a/core/errors/errors.go b/core/errors/errors.go index d96f277a..56611fde 100644 --- a/core/errors/errors.go +++ b/core/errors/errors.go @@ -37,6 +37,16 @@ func New(errMsg string) Error { } } +// NewFromErr same as `New` but pointer for nil checks without the need of the `Return()` function. +func NewFromErr(err error) *Error { + if err == nil { + return nil + } + + errp := New(err.Error()) + return &errp +} + // Equal returns true if "e" and "e2" are matched, by their IDs. // It will always returns true if the "e2" is a children of "e" // or the error messages are exactly the same, otherwise false. @@ -123,6 +133,17 @@ func (e Error) With(err error) error { return e.Format(err.Error()) } +// Ignore will ignore the "err" and return nil. +func (e Error) Ignore(err error) error { + if err == nil { + return e + } + if e.Error() == err.Error() { + return nil + } + return e +} + // Panic output the message and after panics. func (e Error) Panic() { _, fn, line, _ := runtime.Caller(1) diff --git a/core/errors/reporter.go b/core/errors/reporter.go index 8da3f59a..859993a2 100644 --- a/core/errors/reporter.go +++ b/core/errors/reporter.go @@ -106,12 +106,12 @@ func (r *Reporter) Describe(format string, err error) { // PrintStack prints all the errors to the given "printer". // Returns itself in order to be used as printer and return the full error in the same time. -func (r Reporter) PrintStack(printer func(string, ...interface{})) error { +func (r *Reporter) PrintStack(printer func(string, ...interface{})) error { return PrintAndReturnErrors(r, printer) } // Stack returns the list of the errors in the stack. -func (r Reporter) Stack() []Error { +func (r *Reporter) Stack() []Error { return r.wrapper.Stack } @@ -127,12 +127,12 @@ func (r *Reporter) addStack(stack []Error) { } // Error implements the error, returns the full error string. -func (r Reporter) Error() string { +func (r *Reporter) Error() string { return r.wrapper.Error() } // Return returns nil if the error is empty, otherwise returns the full error. -func (r Reporter) Return() error { +func (r *Reporter) Return() error { if r.Error() == "" { return nil } diff --git a/core/host/supervisor.go b/core/host/supervisor.go index 7d98f4ba..cddf2ff7 100644 --- a/core/host/supervisor.go +++ b/core/host/supervisor.go @@ -26,9 +26,18 @@ type Supervisor struct { mu sync.Mutex - onServe []func(TaskHost) - onErr []func(error) - onShutdown []func() + onServe []func(TaskHost) + // IgnoreErrors should contains the errors that should be ignored + // on both serve functions return statements and error handlers. + // + // i.e: http.ErrServerClosed.Error(). + // + // Note that this will match the string value instead of the equality of the type's variables. + // + // Defaults to empty. + IgnoredErrors []string + onErr []func(error) + onShutdown []func() } // New returns a new host supervisor @@ -103,17 +112,31 @@ func (su *Supervisor) RegisterOnError(cb func(error)) { su.mu.Unlock() } -func (su *Supervisor) notifyErr(err error) { - // if err == http.ErrServerClosed { - // su.notifyShutdown() - // return - // } +func (su *Supervisor) validateErr(err error) error { + if err == nil { + return nil + } su.mu.Lock() - for _, f := range su.onErr { - go f(err) + defer su.mu.Unlock() + + for _, e := range su.IgnoredErrors { + if err.Error() == e { + return nil + } + } + return err +} + +func (su *Supervisor) notifyErr(err error) { + err = su.validateErr(err) + if err != nil { + su.mu.Lock() + for _, f := range su.onErr { + go f(err) + } + su.mu.Unlock() } - su.mu.Unlock() } // RegisterOnServe registers a function to call on @@ -156,7 +179,7 @@ func (su *Supervisor) supervise(blockFunc func() error) error { } } - return err // start the server + return su.validateErr(err) } // Serve accepts incoming connections on the Listener l, creating a @@ -171,7 +194,6 @@ func (su *Supervisor) supervise(blockFunc func() error) error { // Serve always returns a non-nil error. After Shutdown or Close, the // returned error is http.ErrServerClosed. func (su *Supervisor) Serve(l net.Listener) error { - return su.supervise(func() error { return su.Server.Serve(l) }) } diff --git a/iris.go b/iris.go index 1eb298c3..987bfe93 100644 --- a/iris.go +++ b/iris.go @@ -33,7 +33,7 @@ import ( const ( // Version is the current version number of the Iris Web Framework. - Version = "8.0.0" + Version = "8.0.1" ) // HTTP status codes as registered with IANA. @@ -361,6 +361,8 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { host.RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout)) } + su.IgnoredErrors = append(su.IgnoredErrors, app.config.IgnoreServerErrors...) + app.Hosts = append(app.Hosts, su) return su @@ -512,7 +514,7 @@ func (app *Application) Build() error { // ErrServerClosed is returned by the Server's Serve, ServeTLS, ListenAndServe, // and ListenAndServeTLS methods after a call to Shutdown or Close. // -// Conversion for the http.ErrServerClosed. +// A shortcut for the `http#ErrServerClosed`. var ErrServerClosed = http.ErrServerClosed // Run builds the framework and starts the desired `Runner` with or without configuration edits. @@ -523,7 +525,9 @@ var ErrServerClosed = http.ErrServerClosed // then create a new host and run it manually by `go NewHost(*http.Server).Serve/ListenAndServe` etc... // or use an already created host: // h := NewHost(*http.Server) -// Run(Raw(h.ListenAndServe), WithCharset("UTF-8"), WithRemoteAddrHeader("CF-Connecting-IP")) +// Run(Raw(h.ListenAndServe), WithCharset("UTF-8"), +// WithRemoteAddrHeader("CF-Connecting-IP"), +// WithoutServerError(iris.ErrServerClosed)) // // The Application can go online with any type of server or iris's host with the help of // the following runners: @@ -536,7 +540,6 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error { } app.Configure(withOrWithout...) - // this will block until an error(unless supervisor's DeferFlow called from a Task). err := serve(app) if err != nil {