diff --git a/HISTORY.md b/HISTORY.md index b1e08d29..37e8bcae 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -454,7 +454,7 @@ New Context Methods: Breaking Changes: - +- `Context.StreamWriter(writer func(w io.Writer) bool)` changed to `StreamWriter(writer func(w io.Writer) error) error` and it's now the `Context.Request().Context().Done()` channel that is used to receive any close connection/manual cancel signals, instead of the deprecated `ResponseWriter().CloseNotify()` one. Same for the `Context.OnClose` and `Context.OnCloseConnection` methods. - Fixed handler's error response not be respected when response recorder or gzip writer was used instead of the common writer. Fixes [#1531](https://github.com/kataras/iris/issues/1531). It contains a **BREAKING CHANGE** of: the new `Configuration.ResetOnFireErrorCode` field should be set **to true** in order to behave as it used before this update (to reset the contents on recorder or gzip writer). - `Context.String()` (rarely used by end-developers) it does not return a unique string anymore, to achieve the old representation you must call the new `Context.SetID` method first. - `iris.CookieEncode` and `CookieDecode` are replaced with the `iris.CookieEncoding`. diff --git a/_examples/response-writer/stream-writer/main.go b/_examples/response-writer/stream-writer/main.go index c7a6c558..54c44b2f 100644 --- a/_examples/response-writer/stream-writer/main.go +++ b/_examples/response-writer/stream-writer/main.go @@ -1,13 +1,15 @@ package main import ( - "fmt" // just an optional helper + "errors" "io" "time" // showcase the delay "github.com/kataras/iris/v12" ) +var errDone = errors.New("done") + func main() { app := iris.New() @@ -17,15 +19,21 @@ func main() { i := 0 ints := []int{1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 23, 29} // Send the response in chunks and wait for half a second between each chunk. - ctx.StreamWriter(func(w io.Writer) bool { - fmt.Fprintf(w, "Message number %d
", ints[i]) + err := ctx.StreamWriter(func(w io.Writer) error { + ctx.Writef("Message number %d
", ints[i]) time.Sleep(500 * time.Millisecond) // simulate delay. if i == len(ints)-1 { - return false // close and flush + return errDone // ends the loop. } i++ - return true // continue write + return nil // continue write }) + + if err != errDone { + // Test it by canceling the request before the stream ends: + // [ERRO] $DATETIME stream: context canceled. + ctx.Application().Logger().Errorf("stream: %v", err) + } }) type messageNumber struct { @@ -33,7 +41,6 @@ func main() { } app.Get("/alternative", func(ctx iris.Context) { - ctx.ContentType("application/json") ctx.Header("Transfer-Encoding", "chunked") i := 0 ints := []int{1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 23, 29} @@ -52,3 +59,10 @@ func main() { app.Listen(":8080") } + +/* +Look the following methods too: +- Context.OnClose(callback) +- Context.OnConnectionClose(callback) and +- Context.Request().Context().Done()/.Err() too +*/ diff --git a/context/context.go b/context/context.go index dddee066..4d1c5757 100644 --- a/context/context.go +++ b/context/context.go @@ -286,15 +286,14 @@ type Context interface { // it will also fire the specified error code handler. StopWithProblem(statusCode int, problem Problem) - // OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev) + // OnConnectionClose registers the "cb" function which will fire + // (on its own goroutine, no need to be registered goroutine by the end-dev) // when the underlying connection has gone away. // // This mechanism can be used to cancel long operations on the server // if the client has disconnected before the response is ready. // - // It depends on the `http#CloseNotify`. - // CloseNotify may wait to notify until Request.Body has been - // fully read. + // It depends on the Request's Context.Done() channel. // // After the main Handler has returned, there is no guarantee // that the channel receives a value. @@ -303,8 +302,6 @@ type Context interface { // The "cb" will not fire for sure if the output value is false. // // Note that you can register only one callback for the entire request handler chain/per route. - // - // Look the `ResponseWriter#CloseNotifier` for more. OnConnectionClose(fnGoroutine func()) bool // OnClose registers the callback function "cb" to the underline connection closing event using the `Context#OnConnectionClose` // and also in the end of the request handler using the `ResponseWriter#SetBeforeFlush`. @@ -775,10 +772,7 @@ type Context interface { // * if response body is streamed from slow external sources. // * if response body must be streamed to the client in chunks. // (aka `http server push`). - // - // receives a function which receives the response writer - // and returns false when it should stop writing, otherwise true in order to continue - StreamWriter(writer func(w io.Writer) bool) + StreamWriter(writer func(w io.Writer) error) error // +------------------------------------------------------------+ // | Body Writers with compression | @@ -1644,15 +1638,14 @@ func (ctx *context) StopWithProblem(statusCode int, problem Problem) { ctx.Problem(problem) } -// OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev) +// OnConnectionClose registers the "cb" function which will fire +// (on its own goroutine, no need to be registered goroutine by the end-dev) // when the underlying connection has gone away. // // This mechanism can be used to cancel long operations on the server // if the client has disconnected before the response is ready. // -// It depends on the `http#CloseNotify`. -// CloseNotify may wait to notify until Request.Body has been -// fully read. +// It depends on the Request's Context.Done() channel. // // After the main Handler has returned, there is no guarantee // that the channel receives a value. @@ -1661,25 +1654,21 @@ func (ctx *context) StopWithProblem(statusCode int, problem Problem) { // The "cb" will not fire for sure if the output value is false. // // Note that you can register only one callback for the entire request handler chain/per route. -// -// Look the `ResponseWriter#CloseNotifier` for more. func (ctx *context) OnConnectionClose(cb func()) bool { if cb == nil { return false } - // Note that `ctx.ResponseWriter().CloseNotify()` can already do the same - // but it returns a channel which will never fire if it the protocol version is not compatible, - // here we don't want to allocate an empty channel, just skip it. - notifier, ok := ctx.writer.CloseNotifier() - if !ok { + notifyClose := ctx.Request().Context().Done() + if notifyClose == nil { return false } - notify := notifier.CloseNotify() go func() { - <-notify + <-notifyClose cb() + // Callers can check the error + // through `Context.Request().Context().Err()`. }() return true @@ -3294,23 +3283,20 @@ func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, er // * if response body is streamed from slow external sources. // * if response body must be streamed to the client in chunks. // (aka `http server push`). -// -// receives a function which receives the response writer -// and returns false when it should stop writing, otherwise true in order to continue -func (ctx *context) StreamWriter(writer func(w io.Writer) bool) { - w := ctx.writer - notifyClosed := w.CloseNotify() +func (ctx *context) StreamWriter(writer func(w io.Writer) error) error { + cancelCtx := ctx.Request().Context() + notifyClosed := cancelCtx.Done() + for { select { // response writer forced to close, exit. case <-notifyClosed: - return + return cancelCtx.Err() default: - shouldContinue := writer(w) - w.Flush() - if !shouldContinue { - return + if err := writer(ctx.writer); err != nil { + return err } + ctx.writer.Flush() } } }