diff --git a/HISTORY.md b/HISTORY.md index 37e8bcae..5d8c82db 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -425,6 +425,8 @@ New Package-level Variables: New Context Methods: +- `Context.Clone() Context` returns a copy of the Context. +- `Context.IsCanceled() bool` reports whether the request has been canceled by the client. - `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too). - `Context.GzipReader(enable bool)` method and `iris.GzipReader` middleware to enable future request read body calls to decompress data using gzip, [example](_examples/request-body/read-gzip). - `Context.RegisterDependency(v interface{})` and `Context.UnregisterDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware. @@ -448,12 +450,12 @@ New Context Methods: - `Context.ReadJSONProtobuf(ptr, ...options)` binds JSON request body to a proto message - `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct - `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type -- `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead - `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)` - `Context.Controller() reflect.Value` returns the current MVC Controller value. Breaking Changes: +- `Context.OnClose` and `Context.OnCloseConnection` now both accept an `iris.Handler` instead of a simple `func()` as their callback. - `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. diff --git a/_examples/response-writer/sse/main.go b/_examples/response-writer/sse/main.go index c956aa32..a07419eb 100644 --- a/_examples/response-writer/sse/main.go +++ b/_examples/response-writer/sse/main.go @@ -76,8 +76,7 @@ func (b *Broker) ServeHTTP(ctx iris.Context) { flusher, ok := ctx.ResponseWriter().Flusher() if !ok { - ctx.StatusCode(iris.StatusHTTPVersionNotSupported) - ctx.WriteString("Streaming unsupported!") + ctx.StopWithText(iris.StatusHTTPVersionNotSupported, "Streaming unsupported!") return } diff --git a/context/context.go b/context/context.go index 4d1c5757..4532c569 100644 --- a/context/context.go +++ b/context/context.go @@ -21,7 +21,6 @@ import ( "regexp" "strconv" "strings" - "sync" "time" "unsafe" @@ -98,6 +97,13 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error { // context.Context is very extensible and developers can override // its methods if that is actually needed. type Context interface { + // Clone returns a copy of the context that + // can be safely used outside the request's scope. + // Note that if the request-response lifecycle terminated + // or request canceled by the client (can be checked by `ctx.IsCanceled()`) + // then the response writer is totally useless. + // The http.Request pointer value is shared. + Clone() Context // BeginRequest is executing once for each request // it should prepare the (new or acquired from pool) context's fields for the new request. // Do NOT call it manually. Framework calls it automatically. @@ -112,14 +118,42 @@ type Context interface { // EndRequest is executing once after a response to the request was sent and this context is useless or released. // Do NOT call it manually. Framework calls it automatically. // - // 1. executes the Defer function (if any). + // 1. executes the OnClose function (if any). // 2. flushes the response writer's result or fire any error handler. // 3. releases the response writer. EndRequest() - // Defer executes a handler on this Context right before the request ends. - // The `StopExecution` does not effect the execution of this defer handler. - // The "h" runs before `FireErrorCode` (when response status code is not successful). - Defer(Handler) + // IsCanceled reports whether the client canceled the request + // or the underlying connection has gone. + // Note that it will always return true + // when called from a goroutine after the request-response lifecycle. + IsCanceled() bool + // OnConnectionClose registers the "cb" Handler + // which will be fired on its on goroutine on a cloned Context + // when the underlying connection has gone away. + // + // The code inside the given callback is running on its own routine, + // as explained above, therefore the callback should NOT + // try to access to handler's Context response writer. + // + // 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 Request's Context.Done() channel. + // + // Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported). + // The "cb" will not fire for sure if the output value is false. + // + // Note that you can register only one callback per route. + // + // See `OnClose` too. + OnConnectionClose(Handler) bool + // OnClose registers a callback which + // will be fired when the underlying connection has gone away(request canceled) + // on its own goroutine or in the end of the request-response lifecylce + // on the handler's routine itself (Context access). + // + // See `OnConnectionClose` too. + OnClose(Handler) // ResponseWriter returns an http.ResponseWriter compatible response writer, as expected. ResponseWriter() ResponseWriter @@ -286,31 +320,6 @@ 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) - // 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 Request's Context.Done() channel. - // - // After the main Handler has returned, there is no guarantee - // that the channel receives a value. - // - // Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported). - // 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. - 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`. - // Note that you can register only one callback for the entire request handler chain/per route. - // Note that the "cb" will only be called once. - // - // Look the `Context#OnConnectionClose` and `ResponseWriter#SetBeforeFlush` for more. - OnClose(cb func()) - // +------------------------------------------------------------+ // | Current "user/request" storage | // | and share information between the handlers - Values(). | @@ -1251,7 +1260,6 @@ type context struct { request *http.Request // the current route registered to this request path. currentRoute RouteReadOnly - deferFunc Handler // the local key-value storage params RequestParams // url named parameters. @@ -1274,6 +1282,30 @@ func NewContext(app Application) Context { return &context{app: app} } +// Clone returns a copy of the context that +// can be safely used outside the request's scope. +// Note that if the request-response lifecycle terminated +// or request canceled by the client (can be checked by `ctx.IsCanceled()`) +// then the response writer is totally useless. +// The http.Request pointer value is shared. +func (ctx *context) Clone() Context { + valuesCopy := make(memstore.Store, len(ctx.values)) + copy(valuesCopy, ctx.values) + + paramsCopy := make(memstore.Store, len(ctx.params.Store)) + copy(paramsCopy, ctx.params.Store) + + return &context{ + app: ctx.app, + values: valuesCopy, + params: RequestParams{Store: paramsCopy}, + writer: ctx.writer.Clone(), + request: ctx.request, + currentHandlerIndex: stopExecutionIndex, + currentRoute: ctx.currentRoute, + } +} + // BeginRequest is executing once for each request // it should prepare the (new or acquired from pool) context's fields for the new request. // Do NOT call it manually. Framework calls it automatically. @@ -1291,7 +1323,6 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.params.Store = ctx.params.Store[0:0] ctx.request = r ctx.currentHandlerIndex = 0 - ctx.deferFunc = nil ctx.writer = AcquireResponseWriter() ctx.writer.BeginResponse(w) } @@ -1299,14 +1330,10 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { // EndRequest is executing once after a response to the request was sent and this context is useless or released. // Do NOT call it manually. Framework calls it automatically. // -// 1. executes the Defer function (if any). +// 1. executes the OnClose function (if any). // 2. flushes the response writer's result or fire any error handler. // 3. releases the response writer. func (ctx *context) EndRequest() { - if ctx.deferFunc != nil { - ctx.deferFunc(ctx) - } - if !ctx.app.ConfigurationReadOnly().GetDisableAutoFireStatusCode() && StatusCodeNotSuccessful(ctx.GetStatusCode()) { ctx.app.FireErrorCode(ctx) @@ -1316,11 +1343,88 @@ func (ctx *context) EndRequest() { ctx.writer.EndResponse() } -// Defer executes a handler on this Context right before the request ends. -// The `StopExecution` does not effect the execution of this defer handler. -// The "h" runs before `FireErrorCode` (when response status code is not successful). -func (ctx *context) Defer(h Handler) { - ctx.deferFunc = h +// IsCanceled reports whether the client canceled the request +// or the underlying connection has gone. +// Note that it will always return true +// when called from a goroutine after the request-response lifecycle. +func (ctx *context) IsCanceled() bool { + if reqCtx := ctx.request.Context(); reqCtx != nil { + err := reqCtx.Err() + if errors.Is(err, stdContext.Canceled) { + return true + } + } + + return false +} + +// OnConnectionClose registers the "cb" Handler +// which will be fired on its on goroutine on a cloned Context +// when the underlying connection has gone away. +// +// The code inside the given callback is running on its own routine, +// as explained above, therefore the callback should NOT +// try to access to handler's Context response writer. +// +// 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 Request's Context.Done() channel. +// +// Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported). +// The "cb" will not fire for sure if the output value is false. +// +// Note that you can register only one callback per route. +// +// See `OnClose` too. +func (ctx *context) OnConnectionClose(cb Handler) bool { + if cb == nil { + return false + } + + reqCtx := ctx.Request().Context() + if reqCtx == nil { + return false + } + + notifyClose := reqCtx.Done() + if notifyClose == nil { + return false + } + + go func() { + <-notifyClose + // Note(@kataras): No need to clone if not canceled, + // EndRequest will be called on the end of the handler chain, + // no matter the cancelation. + // therefore the context will still be there. + cb(ctx.Clone()) + }() + + return true +} + +// OnClose registers a callback which +// will be fired when the underlying connection has gone away(request canceled) +// on its own goroutine or in the end of the request-response lifecylce +// on the handler's routine itself (Context access). +// +// See `OnConnectionClose` too. +func (ctx *context) OnClose(cb Handler) { + if cb == nil { + return + } + + ctx.OnConnectionClose(cb) + + fn := func() { + if !ctx.IsCanceled() { + // If the callback not fired by OnConnectionClose already. + cb(ctx) + } + } + + ctx.writer.SetBeforeFlush(fn) } // ResponseWriter returns an http.ResponseWriter compatible response writer, as expected. @@ -1638,81 +1742,6 @@ 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) -// 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 Request's Context.Done() channel. -// -// After the main Handler has returned, there is no guarantee -// that the channel receives a value. -// -// Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported). -// 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. -func (ctx *context) OnConnectionClose(cb func()) bool { - if cb == nil { - return false - } - - notifyClose := ctx.Request().Context().Done() - if notifyClose == nil { - return false - } - - go func() { - <-notifyClose - cb() - // Callers can check the error - // through `Context.Request().Context().Err()`. - }() - - return true -} - -// 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`. -// Note that you can register only one callback for the entire request handler chain/per route. -// -// Note that the "cb" will only be called once. -// -// Look the `Context#OnConnectionClose` and `ResponseWriter#SetBeforeFlush` for more. -func (ctx *context) OnClose(cb func()) { - if cb == nil { - return - } - - once := new(sync.Once) - - callOnce := func() { - once.Do(cb) - } - - // Register the on underline connection close handler first. - ctx.OnConnectionClose(callOnce) - - // Author's notes: - // This is fired on `ctx.ResponseWriter().FlushResponse()` which is fired by the framework automatically, internally, on the end of request handler(s), - // it is not fired on the underline streaming function of the writer: `ctx.ResponseWriter().Flush()` (which can be fired more than one if streaming is supported by the client). - // The `FlushResponse` is called only once, so add the "cb" here, no need to add done request handlers each time `OnClose` is called by the end-dev. - // - // Don't allow more than one because we don't allow that on `OnConnectionClose` too: - // old := ctx.writer.GetBeforeFlush() - // if old != nil { - // ctx.writer.SetBeforeFlush(func() { - // old() - // cb() - // }) - // return - // } - - ctx.writer.SetBeforeFlush(callOnce) -} - // +------------------------------------------------------------+ // | Current "user/request" storage | // | and share information between the handlers - Values(). |