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()
}
}
}