add Context.Clone and change the input argument of Context.OnClose and OnCloseConnection ( the un-released Defer has been removed, OnClose can do its job)

Former-commit-id: 7b606e285f4b9de24338ea96d482cf1f7c4907a3
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-07-02 20:31:34 +03:00
parent 2d9485326b
commit 85fc0f5dab
3 changed files with 151 additions and 121 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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(). |