diff --git a/HISTORY.md b/HISTORY.md
index 96e4e4e4..e4d632e1 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -2,6 +2,26 @@
**How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`.
+## 6.0.0 -> 6.0.1
+
+We had(for 2 days) one ResponseWriter which has special and unique features, but it slowed the execution a little bit, so I had to think more about it, I want to keep iris as the fastest http/2 web framework, well-designed and also to be usable and very easy for new programmers, performance vs design is tough decision. I choose performance most of the times but golang gives us the way to have a good design with that too.
+
+I had to convert that ResponseWriter to a 'big but simple golang' interface and split the behavior into two parts, one will be the default and fast response writer, and the other will be the most useful response writer + transactions = iris.ResponseRecorder (no other framework or library have these features as far as I know). At the same time I had to provide an easy one-call way to wrap the basic response writer to a response recorder and set it to the context or to the whole app.
+
+> Of course I give the green light to other authors to copy these response writers as I already did with the whole source code and I'm happy to see my code exists into other famous web frameworks even when they don't notice my name anywhere :)
+
+- **response_writer.go**: is the response writer as you knew it with iris' bonus like the `StatusCode() int` which returns the http status code (useful for middleware which needs to know the previous status code), `WriteHeader` which doesn't let you write the status code more than once and so on.
+
+- **response_recorder.go**: is the response writer used by `Transactions` but you can use it by calling the `context.Record/Redorder/IsRecording()`. It lets you `ResetBody` , `ResetHeaders and cookies`, set the `status code` at any time (before or after its Write method) and more.
+
+
+Transform the responseWriter to a ResponseRecorder is ridiculous easily, depending on yours preferences select one of these methods:
+
+- context call (lifetime only inside route's handlers/middleware): `context.Record();` which will convert the context.ResponseWriter to a ResponseRecorder. All previous methods works as before but if you want to `ResetBody/Reset/ResetHeaders/SetBody/SetBodyString` you will have to use the `w := context.Recorder()` or just cast the context.ResponseWriter to a pointer of iris.ResponseRecorder.
+
+- middleware (global, per party, per route...): `iris.UseGlobal(iris.Recorder)`/`app := iris.New(); app.UseGlobal(iris.Recorder)` or `iris.Get("/mypath", iris.Recorder, myPathHandler)`
+
+
## v5/fasthttp -> 6.0.0
diff --git a/README.md b/README.md
index 348d975b..fa5cd833 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
-
+
@@ -823,7 +823,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning
------------
-Current: **v6.0.0**
+Current: **v6.0.1**
Stable: **[v5/fasthttp](https://github.com/kataras/iris/tree/5.0.0)**
diff --git a/context.go b/context.go
index a35083c0..cbd49ace 100644
--- a/context.go
+++ b/context.go
@@ -137,7 +137,7 @@ type (
// Context is resetting every time a request is coming to the server
// it is not good practice to use this object in goroutines, for these cases use the .Clone()
Context struct {
- ResponseWriter *ResponseWriter
+ ResponseWriter // *responseWriter by default, when record is on then *ResponseRecorder
Request *http.Request
values requestValues
framework *Framework
@@ -473,11 +473,6 @@ func (ctx *Context) ReadForm(formObject interface{}) error {
return errReadBody.With(formBinder.Decode(values, formObject))
}
-// ResetBody resets the body of the response
-func (ctx *Context) ResetBody() {
- ctx.ResponseWriter.ResetBody()
-}
-
/* Response */
// SetContentType sets the response writer's header key 'Content-Type' to a given value(s)
@@ -492,7 +487,7 @@ func (ctx *Context) SetHeader(k string, v string) {
// SetStatusCode sets the status code header to the response
//
-// NOTE: Iris takes cares of multiple header writing
+// same as .WriteHeader, iris takes cares of your status code seriously
func (ctx *Context) SetStatusCode(statusCode int) {
ctx.ResponseWriter.WriteHeader(statusCode)
}
@@ -519,7 +514,7 @@ func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) {
ctx.Log("Trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect)
}
}
- http.Redirect(ctx.ResponseWriter.ResponseWriter, ctx.Request, urlToRedirect, httpStatus)
+ http.Redirect(ctx.ResponseWriter, ctx.Request, urlToRedirect, httpStatus)
}
// RedirectTo does the same thing as Redirect but instead of receiving a uri or path it receives a route name
@@ -554,38 +549,6 @@ func (ctx *Context) EmitError(statusCode int) {
ctx.StopExecution()
}
-// -------------------------------------------------------------------------------------
-// -------------------------------------------------------------------------------------
-// -----------------------------Raw write methods---------------------------------------
-// -------------------------------------------------------------------------------------
-// -------------------------------------------------------------------------------------
-
-// Write writes the contents to the response writer.
-//
-// Returns the number of bytes written and any write error encountered
-func (ctx *Context) Write(contents []byte) (n int, err error) {
- return ctx.ResponseWriter.Write(contents)
-}
-
-// Writef formats according to a format specifier and writes to the response.
-//
-// Returns the number of bytes written and any write error encountered
-func (ctx *Context) Writef(format string, a ...interface{}) (n int, err error) {
- return fmt.Fprintf(ctx.ResponseWriter, format, a...)
-}
-
-// WriteString writes a simple string to the response.
-//
-// Returns the number of bytes written and any write error encountered
-func (ctx *Context) WriteString(s string) (n int, err error) {
- return io.WriteString(ctx.ResponseWriter, s)
-}
-
-// SetBodyString writes a simple string to the response.
-func (ctx *Context) SetBodyString(s string) {
- ctx.ResponseWriter.SetBodyString(s)
-}
-
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// -------------------------Context's gzip inline response writer ----------------------
@@ -690,15 +653,20 @@ func (ctx *Context) RenderTemplateSource(status int, src string, binding interfa
// RenderWithStatus builds up the response from the specified template or a serialize engine.
// Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engines
func (ctx *Context) RenderWithStatus(status int, name string, binding interface{}, options ...map[string]interface{}) (err error) {
+
+ if _, shouldFirstStatusCode := ctx.ResponseWriter.(*responseWriter); shouldFirstStatusCode {
+ ctx.SetStatusCode(status)
+ }
+
if strings.IndexByte(name, '.') > -1 { //we have template
err = ctx.framework.templates.renderFile(ctx, name, binding, options...)
} else {
err = ctx.renderSerialized(name, binding, options...)
}
-
- if err == nil {
- ctx.SetStatusCode(status)
- }
+ // we don't care for the last one it will not be written more than one if we have the *responseWriter
+ ///TODO:
+ // if we have ResponseRecorder order doesn't matters but I think the transactions have bugs , for now let's keep it here because it 'fixes' one of them...
+ ctx.SetStatusCode(status)
return
}
@@ -1212,10 +1180,38 @@ func (ctx *Context) MaxAge() int64 {
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
-// --------------------------------Transactions-----------------------------------------
+// ---------------------------Transactions & Response Writer Recording------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
+// Record transforms the context's basic and direct responseWriter to a ResponseRecorder
+// which can be used to reset the body, reset headers, get the body,
+// get & set the status code at any time and more
+func (ctx *Context) Record() {
+ if w, ok := ctx.ResponseWriter.(*responseWriter); ok {
+ ctx.ResponseWriter = acquireResponseRecorder(w)
+ }
+}
+
+// Recorder returns the context's ResponseRecorder
+// if not recording then it starts recording and returns the new context's ResponseRecorder
+func (ctx *Context) Recorder() *ResponseRecorder {
+ ctx.Record()
+ return ctx.ResponseWriter.(*ResponseRecorder)
+}
+
+// IsRecording returns the response recorder and a true value
+// when the response writer is recording the status code, body, headers and so on,
+// else returns nil and false
+func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
+ //NOTE:
+ // two return values in order to minimize the if statement:
+ // if (Recording) then writer = Recorder()
+ // instead we do: recorder,ok = Recording()
+ rr, ok := ctx.ResponseWriter.(*ResponseRecorder)
+ return rr, ok
+}
+
// skipTransactionsContextKey set this to any value to stop executing next transactions
// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions()
const skipTransactionsContextKey = "__IRIS_TRANSACTIONS_SKIP___"
@@ -1259,6 +1255,10 @@ func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) {
if ctx.TransactionsSkipped() {
return
}
+
+ // start recording in order to be able to control the full response writer
+ ctx.Record()
+
// get a transaction scope from the pool by passing the temp context/
t := newTransaction(ctx)
defer func() {
@@ -1273,6 +1273,7 @@ func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) {
// write the temp contents to the original writer
t.Context.ResponseWriter.writeTo(ctx.ResponseWriter)
+
// give back to the transaction the original writer (SetBeforeFlush works this way and only this way)
// this is tricky but nessecery if we want ctx.EmitError to work inside transactions
t.Context.ResponseWriter = ctx.ResponseWriter
diff --git a/context_test.go b/context_test.go
index 215cf036..c9bf836d 100644
--- a/context_test.go
+++ b/context_test.go
@@ -361,7 +361,6 @@ func TestContextRedirectTo(t *testing.T) {
args = append(args, s)
}
}
- //println("Redirecting to: " + routeName + " with path: " + Path(routeName, args...))
ctx.RedirectTo(routeName, args...)
})
@@ -691,7 +690,9 @@ func TestTransactions(t *testing.T) {
}
successTransaction := func(scope *iris.Transaction) {
-
+ if scope.Context.Request.RequestURI == "/failAllBecauseOfRequestScopeAndFailure" {
+ t.Fatalf("We are inside successTransaction but the previous REQUEST SCOPED TRANSACTION HAS FAILED SO THiS SHOULD NOT BE RAN AT ALL")
+ }
scope.Context.HTML(iris.StatusOK,
secondTransactionSuccessHTMLMessage)
// * if we don't have any 'throw error' logic then no need of scope.Complete()
diff --git a/http.go b/http.go
index 86b7f15c..fbc5d659 100644
--- a/http.go
+++ b/http.go
@@ -886,7 +886,9 @@ func (mux *serveMux) registerError(statusCode int, handler Handler) {
mux.mu.Lock()
func(statusCode int, handler Handler) {
mux.errorHandlers[statusCode] = HandlerFunc(func(ctx *Context) {
- ctx.ResetBody()
+ if w, ok := ctx.IsRecording(); ok {
+ w.Reset()
+ }
ctx.SetStatusCode(statusCode)
handler.Serve(ctx)
})
@@ -900,14 +902,15 @@ func (mux *serveMux) fireError(statusCode int, ctx *Context) {
errHandler := mux.errorHandlers[statusCode]
if errHandler == nil {
errHandler = HandlerFunc(func(ctx *Context) {
- ctx.ResponseWriter.Reset()
+ if w, ok := ctx.IsRecording(); ok {
+ w.Reset()
+ }
ctx.SetStatusCode(statusCode)
- ctx.SetBodyString(statusText[statusCode])
+ ctx.WriteString(statusText[statusCode])
})
mux.errorHandlers[statusCode] = errHandler
}
mux.mu.Unlock()
-
errHandler.Serve(ctx)
}
@@ -1113,8 +1116,16 @@ var (
errParseTLS = errors.New("Couldn't load TLS, certFile=%q, keyFile=%q. Trace: %s")
)
+// TCPKeepAlive returns a new tcp keep alive Listener
+func TCPKeepAlive(addr string) (net.Listener, error) {
+ ln, err := net.Listen("tcp", ParseHost(addr))
+ if err != nil {
+ return nil, err
+ }
+ return TCPKeepAliveListener{ln.(*net.TCPListener)}, err
+}
+
// TCP4 returns a new tcp4 Listener
-// *tcp6 has some bugs in some operating systems, as reported by Go Community*
func TCP4(addr string) (net.Listener, error) {
return net.Listen("tcp4", ParseHost(addr))
}
@@ -1253,14 +1264,14 @@ type TCPKeepAliveListener struct {
*net.TCPListener
}
-// Accept implements the listener and sets the keep alive period which is 2minutes
+// Accept implements the listener and sets the keep alive period which is 3minutes
func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true)
- tc.SetKeepAlivePeriod(2 * time.Minute)
+ tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
@@ -1283,7 +1294,7 @@ func ParseHost(addr string) string {
} else if osport := os.Getenv("PORT"); osport != "" {
a = ":" + osport
} else {
- a = DefaultServerAddr
+ a = ":http"
}
}
if portIdx := strings.IndexByte(a, ':'); portIdx == 0 {
@@ -1380,6 +1391,8 @@ var ProxyHandler = func(redirectSchemeAndHost string) http.HandlerFunc {
return
}
+ // redirectTo := redirectSchemeAndHost + r.RequestURI
+
http.Redirect(w, r, redirectTo, StatusMovedPermanently)
}
}
@@ -1401,5 +1414,6 @@ func Proxy(proxyAddr string, redirectSchemeAndHost string) func() error {
if ok := <-prx.Available; !ok {
prx.Logger.Panic("Unexpected error: proxy server cannot start, please report this as bug!!")
}
+
return func() error { return prx.Close() }
}
diff --git a/http_test.go b/http_test.go
index 234426fd..8af3b812 100644
--- a/http_test.go
+++ b/http_test.go
@@ -331,7 +331,7 @@ func TestMuxSimple(t *testing.T) {
iris.HandleFunc(r.Method, r.Path, func(ctx *iris.Context) {
ctx.SetStatusCode(r.Status)
if r.Params != nil && len(r.Params) > 0 {
- ctx.SetBodyString(ctx.ParamsSentence())
+ ctx.WriteString(ctx.ParamsSentence())
} else if r.URLParams != nil && len(r.URLParams) > 0 {
if len(r.URLParams) != len(ctx.URLParams()) {
t.Fatalf("Error when comparing length of url parameters %d != %d", len(r.URLParams), len(ctx.URLParams()))
@@ -344,9 +344,9 @@ func TestMuxSimple(t *testing.T) {
paramsKeyVal = paramsKeyVal[0 : len(paramsKeyVal)-1]
}
}
- ctx.SetBodyString(paramsKeyVal)
+ ctx.WriteString(paramsKeyVal)
} else {
- ctx.SetBodyString(r.Body)
+ ctx.WriteString(r.Body)
}
})
diff --git a/iris.go b/iris.go
index 750d7dc7..6cb3297c 100644
--- a/iris.go
+++ b/iris.go
@@ -81,7 +81,7 @@ const (
// IsLongTermSupport flag is true when the below version number is a long-term-support version
IsLongTermSupport = false
// Version is the current version number of the Iris web framework
- Version = "6.0.0"
+ Version = "6.0.1"
banner = ` _____ _
|_ _| (_)
@@ -346,7 +346,7 @@ func Build() {
// Build builds the whole framework's parts together
// DO NOT CALL IT MANUALLY IF YOU ARE NOT:
-// SERVE IRIS BEHIND AN EXTERNAL CUSTOM fasthttp.Server, CAN BE CALLED ONCE PER IRIS INSTANCE FOR YOUR SAFETY
+// SERVE IRIS BEHIND AN EXTERNAL CUSTOM nethttp.Server, CAN BE CALLED ONCE PER IRIS INSTANCE FOR YOUR SAFETY
func (s *Framework) Build() {
s.once.Do(func() {
// .Build, normally*, auto-called after station's listener setted but before the real Serve, so here set the host, scheme
@@ -529,7 +529,7 @@ func (s *Framework) Listen(addr string) {
// this will be set as the front-end listening addr
}
- ln, err := TCP4(addr)
+ ln, err := TCPKeepAlive(addr)
if err != nil {
s.Logger.Panic(err)
}
@@ -608,8 +608,8 @@ func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string)
s.Logger.Panic(err)
}
- // starts a second server which listening on :80 to redirect all requests to the :443 (https://)
- Proxy(":80", "https://"+addr)
+ // starts a second server which listening on HOST:80 to redirect all requests to the HTTPS://HOST:PORT
+ Proxy(ParseHostname(addr)+":80", "https://"+addr)
s.Must(s.Serve(ln))
}
@@ -702,13 +702,15 @@ func ReleaseCtx(ctx *Context) {
// ReleaseCtx puts the Iris' Context back to the pool in order to be re-used
// see .AcquireCtx & .Serve
func (s *Framework) ReleaseCtx(ctx *Context) {
- // flush the body when all finished
+ // flush the body (on recorder) or just the status code (on basic response writer)
+ // when all finished
ctx.ResponseWriter.flushResponse()
ctx.Middleware = nil
ctx.session = nil
ctx.Request = nil
- releaseResponseWriter(ctx.ResponseWriter)
+ ///TODO:
+ ctx.ResponseWriter.releaseMe()
ctx.values.Reset()
s.contextPool.Put(ctx)
@@ -1875,7 +1877,7 @@ func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc
ctx.ResponseWriter.Header().Set(contentType, cType)
ctx.ResponseWriter.Header().Set(lastModified, modtime)
ctx.SetStatusCode(StatusOK)
- ctx.ResponseWriter.SetBody(cacheFav)
+ ctx.Write(cacheFav)
}
reqPath := "/favicon" + path.Ext(fi.Name()) //we could use the filename, but because standards is /favicon.ico/.png.
@@ -2051,9 +2053,11 @@ func (api *muxAPI) OnError(statusCode int, handlerFn HandlerFunc) {
this will be used as the last handler if no other error handler catches the error (by prefix(?))
*/
prevErrHandler = HandlerFunc(func(ctx *Context) {
- ctx.ResetBody()
+ if w, ok := ctx.IsRecording(); ok {
+ w.Reset()
+ }
ctx.SetStatusCode(statusCode)
- ctx.SetBodyString(statusText[statusCode])
+ ctx.WriteString(statusText[statusCode])
})
}
diff --git a/response_recorder.go b/response_recorder.go
new file mode 100644
index 00000000..c5e45e18
--- /dev/null
+++ b/response_recorder.go
@@ -0,0 +1,221 @@
+package iris
+
+import (
+ "fmt"
+ "net/http"
+ "sync"
+)
+
+// Recorder the middleware to enable response writer recording ( *responseWriter -> *ResponseRecorder)
+var Recorder = HandlerFunc(func(ctx *Context) {
+ ctx.Record()
+ ctx.Next()
+})
+
+var rrpool = sync.Pool{New: func() interface{} { return &ResponseRecorder{} }}
+
+func acquireResponseRecorder(underline *responseWriter) *ResponseRecorder {
+ w := rrpool.Get().(*ResponseRecorder)
+ w.responseWriter = underline
+ w.headers = underline.Header()
+ return w
+}
+
+func releaseResponseRecorder(w *ResponseRecorder) {
+ w.ResetBody()
+ if w.responseWriter != nil {
+ releaseResponseWriter(w.responseWriter)
+ }
+
+ rrpool.Put(w)
+}
+
+// A ResponseRecorder is used mostly by context's transactions
+// in order to record and change if needed the body, status code and headers.
+//
+// You are NOT limited to use that too:
+// just call context.ResponseWriter.Recorder()/Record() and
+// response writer will act like context.ResponseWriter.(*iris.ResponseRecorder)
+type ResponseRecorder struct {
+ *responseWriter
+ // these three fields are setted on flushBody which runs only once on the end of the handler execution.
+ // this helps the performance on multi-write and keep tracks the body, status code and headers in order to run each transaction
+ // on its own
+ chunks []byte // keep track of the body in order to be resetable and useful inside custom transactions
+ headers http.Header // the saved headers
+}
+
+var _ ResponseWriter = &ResponseRecorder{}
+
+// Header returns the header map that will be sent by
+// WriteHeader. Changing the header after a call to
+// WriteHeader (or Write) has no effect unless the modified
+// headers were declared as trailers by setting the
+// "Trailer" header before the call to WriteHeader (see example).
+// To suppress implicit response headers, set their value to nil.
+func (w *ResponseRecorder) Header() http.Header {
+ return w.headers
+}
+
+// Writef formats according to a format specifier and writes to the response.
+//
+// Returns the number of bytes written and any write error encountered
+func (w *ResponseRecorder) Writef(format string, a ...interface{}) (n int, err error) {
+ return fmt.Fprintf(w, format, a...)
+}
+
+// WriteString writes a simple string to the response.
+//
+// Returns the number of bytes written and any write error encountered
+func (w *ResponseRecorder) WriteString(s string) (n int, err error) {
+ return w.Write([]byte(s))
+}
+
+// Adds the contents to the body reply, it writes the contents temporarily
+// to a value in order to be flushed at the end of the request,
+// this method give us the opportunity to reset the body if needed.
+//
+// If WriteHeader has not yet been called, Write calls
+// WriteHeader(http.StatusOK) before writing the data. If the Header
+// does not contain a Content-Type line, Write adds a Content-Type set
+// to the result of passing the initial 512 bytes of written data to
+// DetectContentType.
+//
+// Depending on the HTTP protocol version and the client, calling
+// Write or WriteHeader may prevent future reads on the
+// Request.Body. For HTTP/1.x requests, handlers should read any
+// needed request body data before writing the response. Once the
+// headers have been flushed (due to either an explicit Flusher.Flush
+// call or writing enough data to trigger a flush), the request body
+// may be unavailable. For HTTP/2 requests, the Go HTTP server permits
+// handlers to continue to read the request body while concurrently
+// writing the response. However, such behavior may not be supported
+// by all HTTP/2 clients. Handlers should read before writing if
+// possible to maximize compatibility.
+func (w *ResponseRecorder) Write(contents []byte) (int, error) {
+ w.chunks = append(w.chunks, contents...)
+ return len(w.chunks), nil
+}
+
+// Body returns the body tracked from the writer so far
+// do not use this for edit.
+func (w *ResponseRecorder) Body() []byte {
+ return w.chunks
+}
+
+// SetBodyString overrides the body and sets it to a string value
+func (w *ResponseRecorder) SetBodyString(s string) {
+ w.chunks = []byte(s)
+}
+
+// SetBody overrides the body and sets it to a slice of bytes value
+func (w *ResponseRecorder) SetBody(b []byte) {
+ w.chunks = b
+}
+
+// ResetBody resets the response body
+func (w *ResponseRecorder) ResetBody() {
+ w.chunks = w.chunks[0:0]
+}
+
+// ResetHeaders clears the temp headers
+func (w *ResponseRecorder) ResetHeaders() {
+ // original response writer's headers are empty.
+ w.headers = w.responseWriter.Header()
+}
+
+// Reset resets the response body, headers and the status code header
+func (w *ResponseRecorder) Reset() {
+ w.ResetHeaders()
+ w.statusCode = StatusOK
+ w.ResetBody()
+}
+
+// flushResponse the full body, headers and status code to the underline response writer
+// called automatically at the end of each request, see ReleaseCtx
+func (w *ResponseRecorder) flushResponse() {
+ if w.headers != nil {
+ for k, values := range w.headers {
+ for i := range values {
+ w.responseWriter.Header().Add(k, values[i])
+ }
+ }
+ }
+
+ // NOTE: before the responseWriter.Writer in order to:
+ // 1. execute the beforeFlush if != nil
+ // 2. set the status code before the .Write method overides that
+ w.responseWriter.flushResponse()
+
+ if len(w.chunks) > 0 {
+ w.responseWriter.Write(w.chunks)
+ }
+
+}
+
+// Flush sends any buffered data to the client.
+func (w *ResponseRecorder) Flush() {
+ w.flushResponse()
+ w.responseWriter.Flush()
+}
+
+// clone returns a clone of this response writer
+// it copies the header, status code, headers and the beforeFlush finally returns a new ResponseRecorder
+func (w *ResponseRecorder) clone() ResponseWriter {
+ wc := &ResponseRecorder{}
+ wc.headers = w.headers
+ wc.chunks = w.chunks[0:]
+ wc.responseWriter = &(*w.responseWriter) // w.responseWriter.clone().(*responseWriter) //
+ return wc
+}
+
+// writeTo writes a response writer (temp: status code, headers and body) to another response writer
+func (w *ResponseRecorder) writeTo(res ResponseWriter) {
+
+ if to, ok := res.(*ResponseRecorder); ok {
+
+ // set the status code, to is first ( probably an error >=400)
+ if w.statusCode == StatusOK {
+ to.statusCode = w.statusCode
+ }
+
+ if w.beforeFlush != nil {
+ // if to had a before flush, lets combine them
+ if to.beforeFlush != nil {
+ nextBeforeFlush := w.beforeFlush
+ prevBeforeFlush := to.beforeFlush
+ to.beforeFlush = func() {
+ prevBeforeFlush()
+ nextBeforeFlush()
+ }
+ } else {
+ to.beforeFlush = w.beforeFlush
+ }
+ }
+
+ if !to.statusCodeSent {
+ to.statusCodeSent = w.statusCodeSent
+ }
+
+ // append the headers
+ if w.headers != nil {
+ for k, values := range w.headers {
+ for _, v := range values {
+ if to.headers.Get(v) == "" {
+ to.headers.Add(k, v)
+ }
+ }
+ }
+ }
+
+ // append the body
+ if len(w.chunks) > 0 {
+ to.Write(w.chunks)
+ }
+
+ }
+}
+
+func (w *ResponseRecorder) releaseMe() {
+ releaseResponseRecorder(w)
+}
diff --git a/response_writer.go b/response_writer.go
index 5d0c80ae..794194f5 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -2,6 +2,7 @@ package iris
import (
"bufio"
+ "fmt"
"net"
"net/http"
"sync"
@@ -12,14 +13,13 @@ import (
)
type gzipResponseWriter struct {
- http.ResponseWriter
- http.Flusher
+ ResponseWriter
gzipWriter *gzip.Writer
}
var gzpool = sync.Pool{New: func() interface{} { return &gzipResponseWriter{} }}
-func acquireGzipResponseWriter(underline http.ResponseWriter) *gzipResponseWriter {
+func acquireGzipResponseWriter(underline ResponseWriter) *gzipResponseWriter {
w := gzpool.Get().(*gzipResponseWriter)
w.ResponseWriter = underline
w.gzipWriter = fs.AcquireGzipWriter(w.ResponseWriter)
@@ -36,63 +36,80 @@ func (w *gzipResponseWriter) Write(contents []byte) (int, error) {
return w.gzipWriter.Write(contents)
}
-var rpool = sync.Pool{New: func() interface{} { return &ResponseWriter{} }}
+var rpool = sync.Pool{New: func() interface{} { return &responseWriter{statusCode: StatusOK} }}
-func acquireResponseWriter(underline http.ResponseWriter) *ResponseWriter {
- w := rpool.Get().(*ResponseWriter)
+func acquireResponseWriter(underline http.ResponseWriter) *responseWriter {
+ w := rpool.Get().(*responseWriter)
w.ResponseWriter = underline
- w.headers = underline.Header()
return w
}
-func releaseResponseWriter(w *ResponseWriter) {
- w.headers = nil
- w.ResponseWriter = nil
- w.statusCode = 0
+func releaseResponseWriter(w *responseWriter) {
+ w.statusCodeSent = false
w.beforeFlush = nil
- w.ResetBody()
+ w.statusCode = StatusOK
rpool.Put(w)
}
-// A ResponseWriter interface is used by an HTTP handler to
+// ResponseWriter interface is used by the context to serve an HTTP handler to
// construct an HTTP response.
//
// A ResponseWriter may not be used after the Handler.ServeHTTP method
// has returned.
-type ResponseWriter struct {
+type ResponseWriter interface {
+ http.ResponseWriter
+ http.Flusher
+ http.Hijacker
+ http.CloseNotifier
+
+ Writef(format string, a ...interface{}) (n int, err error)
+ WriteString(s string) (n int, err error)
+ SetContentType(cType string)
+ ContentType() string
+ StatusCode() int
+ SetBeforeFlush(cb func())
+ flushResponse()
+ clone() ResponseWriter
+ writeTo(ResponseWriter)
+ releaseMe()
+}
+
+// responseWriter is the basic response writer,
+// it writes directly to the underline http.ResponseWriter
+type responseWriter struct {
+ http.ResponseWriter
+ statusCode int // the saved status code which will be used from the cache service
+ statusCodeSent bool // reply header has been (logically) written
// yes only one callback, we need simplicity here because on EmitError the beforeFlush events should NOT be cleared
// but the response is cleared.
// Sometimes is useful to keep the event,
// so we keep one func only and let the user decide when he/she wants to override it with an empty func before the EmitError (context's behavior)
beforeFlush func()
- http.ResponseWriter
- // these three fields are setted on flushBody which runs only once on the end of the handler execution.
- // this helps the performance on multi-write and keep tracks the body, status code and headers in order to run each transaction
- // on its own
- chunks []byte // keep track of the body in order to be resetable and useful inside custom transactions
- statusCode int // the saved status code which will be used from the cache service
- headers http.Header // the saved headers
}
-// Header returns the header map that will be sent by
-// WriteHeader. Changing the header after a call to
-// WriteHeader (or Write) has no effect unless the modified
-// headers were declared as trailers by setting the
-// "Trailer" header before the call to WriteHeader (see example).
-// To suppress implicit response headers, set their value to nil.
-func (w *ResponseWriter) Header() http.Header {
- return w.headers
-}
+var _ ResponseWriter = &responseWriter{}
// StatusCode returns the status code header value
-func (w *ResponseWriter) StatusCode() int {
+func (w *responseWriter) StatusCode() int {
return w.statusCode
}
-// Adds the contents to the body reply, it writes the contents temporarily
-// to a value in order to be flushed at the end of the request,
-// this method give us the opportunity to reset the body if needed.
+// Writef formats according to a format specifier and writes to the response.
//
+// Returns the number of bytes written and any write error encountered
+func (w *responseWriter) Writef(format string, a ...interface{}) (n int, err error) {
+ w.tryWriteHeader()
+ return fmt.Fprintf(w.ResponseWriter, format, a...)
+}
+
+// WriteString writes a simple string to the response.
+//
+// Returns the number of bytes written and any write error encountered
+func (w *responseWriter) WriteString(s string) (n int, err error) {
+ return w.Write([]byte(s))
+}
+
+// Write writes to the client
// If WriteHeader has not yet been called, Write calls
// WriteHeader(http.StatusOK) before writing the data. If the Header
// does not contain a Content-Type line, Write adds a Content-Type set
@@ -110,66 +127,54 @@ func (w *ResponseWriter) StatusCode() int {
// writing the response. However, such behavior may not be supported
// by all HTTP/2 clients. Handlers should read before writing if
// possible to maximize compatibility.
-func (w *ResponseWriter) Write(contents []byte) (int, error) {
- w.chunks = append(w.chunks, contents...)
- return len(w.chunks), nil
+func (w *responseWriter) Write(contents []byte) (int, error) {
+ w.tryWriteHeader()
+ return w.ResponseWriter.Write(contents)
}
-// Body returns the body tracked from the writer so far
-// do not use this for edit.
-func (w *ResponseWriter) Body() []byte {
- return w.chunks
-}
-
-// SetBodyString overrides the body and sets it to a string value
-func (w *ResponseWriter) SetBodyString(s string) {
- w.chunks = []byte(s)
-}
-
-// SetBody overrides the body and sets it to a slice of bytes value
-func (w *ResponseWriter) SetBody(b []byte) {
- w.chunks = b
-}
-
-// ResetBody resets the response body
-func (w *ResponseWriter) ResetBody() {
- w.chunks = w.chunks[0:0]
-}
-
-// ResetHeaders clears the temp headers
-func (w *ResponseWriter) ResetHeaders() {
- // original response writer's headers are empty.
- w.headers = w.ResponseWriter.Header()
-}
-
-// Reset resets the response body, headers and the status code header
-func (w *ResponseWriter) Reset() {
- w.ResetHeaders()
- w.statusCode = 0
- w.ResetBody()
-}
+// prin to write na benei to write header
+// meta to write den ginete edw
+// prepei omws kai mono me WriteHeader kai xwris Write na pigenei to status code
+// ara...wtf prepei na exw function flushStatusCode kai na elenxei an exei dw9ei status code na to kanei write aliws 200
// WriteHeader sends an HTTP response header with status code.
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
-func (w *ResponseWriter) WriteHeader(statusCode int) {
+func (w *responseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
}
+// SetBeforeFlush registers the unique callback which called exactly before the response is flushed to the client
+func (w *responseWriter) SetBeforeFlush(cb func()) {
+ w.beforeFlush = cb
+}
+
+func (w *responseWriter) flushResponse() {
+ if w.beforeFlush != nil {
+ w.beforeFlush()
+ }
+ w.tryWriteHeader()
+}
+
+func (w *responseWriter) tryWriteHeader() {
+ if !w.statusCodeSent { // by write
+ w.statusCodeSent = true
+ w.ResponseWriter.WriteHeader(w.statusCode)
+ }
+}
+
// ContentType returns the content type, if not setted returns empty string
-func (w *ResponseWriter) ContentType() string {
- return w.headers.Get(contentType)
+func (w *responseWriter) ContentType() string {
+ return w.ResponseWriter.Header().Get(contentType)
}
// SetContentType sets the content type header
-func (w *ResponseWriter) SetContentType(cType string) {
- w.headers.Set(contentType, cType)
+func (w *responseWriter) SetContentType(cType string) {
+ w.ResponseWriter.Header().Set(contentType, cType)
}
-var errHijackNotSupported = errors.New("Hijack is not supported to this response writer!")
-
// Hijack lets the caller take over the connection.
// After a call to Hijack(), the HTTP server library
// will not do anything else with the connection.
@@ -181,47 +186,16 @@ var errHijackNotSupported = errors.New("Hijack is not supported to this response
// already set, depending on the configuration of the
// Server. It is the caller's responsibility to set
// or clear those deadlines as needed.
-func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, isHijacker := w.ResponseWriter.(http.Hijacker); isHijacker {
return h.Hijack()
}
- return nil, nil, errHijackNotSupported
-}
-
-// SetBeforeFlush registers the unique callback which called exactly before the response is flushed to the client
-func (w *ResponseWriter) SetBeforeFlush(cb func()) {
- w.beforeFlush = cb
-}
-
-// flushResponse the full body, headers and status code to the underline response writer
-// called automatically at the end of each request, see ReleaseCtx
-func (w *ResponseWriter) flushResponse() {
-
- if w.beforeFlush != nil {
- w.beforeFlush()
- }
-
- if w.statusCode > 0 {
- w.ResponseWriter.WriteHeader(w.statusCode)
- }
-
- if w.headers != nil {
- for k, values := range w.headers {
- for i := range values {
- w.ResponseWriter.Header().Add(k, values[i])
- }
- }
- }
-
- if len(w.chunks) > 0 {
- w.ResponseWriter.Write(w.chunks)
- }
+ return nil, nil, errors.New("Hijack is not supported to this response writer!")
}
// Flush sends any buffered data to the client.
-func (w *ResponseWriter) Flush() {
- w.flushResponse()
+func (w *responseWriter) Flush() {
// The Flusher interface is implemented by ResponseWriters that allow
// an HTTP handler to flush buffered data to the client.
//
@@ -238,43 +212,60 @@ func (w *ResponseWriter) Flush() {
}
}
+// CloseNotify returns a channel that receives at most a
+// single value (true) when the client connection has gone
+// away.
+//
+// CloseNotify may wait to notify until Request.Body has been
+// fully read.
+//
+// After the Handler has returned, there is no guarantee
+// that the channel receives a value.
+//
+// If the protocol is HTTP/1.1 and CloseNotify is called while
+// processing an idempotent request (such a GET) while
+// HTTP/1.1 pipelining is in use, the arrival of a subsequent
+// pipelined request may cause a value to be sent on the
+// returned channel. In practice HTTP/1.1 pipelining is not
+// enabled in browsers and not seen often in the wild. If this
+// is a problem, use HTTP/2 or only use CloseNotify on methods
+// such as POST.
+func (w *responseWriter) CloseNotify() <-chan bool {
+ return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
+}
+
// clone returns a clone of this response writer
-// it copies the header, status code, headers and the beforeFlush finally returns a new ResponseWriter
-func (w *ResponseWriter) clone() *ResponseWriter {
- wc := &ResponseWriter{}
+// it copies the header, status code, headers and the beforeFlush finally returns a new ResponseRecorder
+func (w *responseWriter) clone() ResponseWriter {
+ wc := &responseWriter{}
wc.ResponseWriter = w.ResponseWriter
wc.statusCode = w.statusCode
- wc.headers = w.headers
- wc.chunks = w.chunks[0:]
wc.beforeFlush = w.beforeFlush
+ wc.statusCodeSent = w.statusCodeSent
return wc
}
// writeTo writes a response writer (temp: status code, headers and body) to another response writer
-func (w *ResponseWriter) writeTo(to *ResponseWriter) {
+func (w *responseWriter) writeTo(to ResponseWriter) {
// set the status code, failure status code are first class
- if w.statusCode > 0 {
- to.statusCode = w.statusCode
+ if w.statusCode >= 400 {
+ to.WriteHeader(w.statusCode)
}
// append the headers
- if w.headers != nil {
- for k, values := range w.headers {
+ if w.Header() != nil {
+ for k, values := range w.Header() {
for _, v := range values {
- if to.headers.Get(v) == "" {
- to.headers.Add(k, v)
+ if to.Header().Get(v) == "" {
+ to.Header().Add(k, v)
}
}
}
}
-
- // append the body
- if len(w.chunks) > 0 {
- to.Write(w.chunks)
- }
-
- if w.beforeFlush != nil {
- to.SetBeforeFlush(w.beforeFlush)
- }
+ // the body is not copied, this writer doesn't supports recording
+}
+
+func (w *responseWriter) releaseMe() {
+ releaseResponseWriter(w)
}
diff --git a/response_writer_test.go b/response_writer_test.go
new file mode 100644
index 00000000..2dc13020
--- /dev/null
+++ b/response_writer_test.go
@@ -0,0 +1,65 @@
+package iris_test
+
+import (
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/httptest"
+ "testing"
+)
+
+// most tests lives inside context_test.go:Transactions, there lives the response writer's full and coblex tests
+func TestResponseWriterBeforeFlush(t *testing.T) {
+ api := iris.New()
+ body := "my body"
+ beforeFlushBody := "body appeneded or setted before callback"
+
+ api.Get("/", func(ctx *iris.Context) {
+ w := ctx.ResponseWriter
+
+ w.SetBeforeFlush(func() {
+ w.WriteString(beforeFlushBody)
+ })
+
+ w.WriteString(body)
+ })
+
+ // recorder can change the status code after write too
+ // it can also be changed everywhere inside the context's lifetime
+ api.Get("/recorder", func(ctx *iris.Context) {
+ w := ctx.Recorder()
+
+ w.SetBeforeFlush(func() {
+ w.SetBodyString(beforeFlushBody)
+ w.WriteHeader(iris.StatusForbidden)
+ })
+
+ w.WriteHeader(iris.StatusOK)
+ w.WriteString(body)
+ })
+
+ e := httptest.New(api, t)
+
+ e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(body + beforeFlushBody)
+ e.GET("/recorder").Expect().Status(iris.StatusForbidden).Body().Equal(beforeFlushBody)
+}
+
+func TestResponseWriterToRecorderMiddleware(t *testing.T) {
+ api := iris.New()
+ beforeFlushBody := "body appeneded or setted before callback"
+ api.UseGlobal(iris.Recorder)
+
+ api.Get("/", func(ctx *iris.Context) {
+ w := ctx.Recorder()
+
+ w.SetBeforeFlush(func() {
+ w.SetBodyString(beforeFlushBody)
+ w.WriteHeader(iris.StatusForbidden)
+ })
+
+ w.WriteHeader(iris.StatusOK)
+ w.WriteString("this will not be sent at all because of SetBodyString")
+ })
+
+ e := httptest.New(api, t)
+
+ e.GET("/").Expect().Status(iris.StatusForbidden).Body().Equal(beforeFlushBody)
+}
diff --git a/transactions.go b/transactions.go
index a21c3627..7193410a 100644
--- a/transactions.go
+++ b/transactions.go
@@ -49,7 +49,7 @@ type Transaction struct {
func newTransaction(from *Context) *Transaction {
tempCtx := *from
writer := tempCtx.ResponseWriter.clone()
- tempCtx.ResponseWriter = writer
+ tempCtx.ResponseWriter = writer //from.ResponseWriter.clone() // &(*tempCtx.ResponseWriter.(*ResponseRecorder))
t := &Transaction{
parent: from,
Context: &tempCtx,
@@ -131,7 +131,7 @@ func (tsf TransactionScopeFunc) EndTransaction(maybeErr TransactionErrResult, ct
// useful for the most cases.
var TransientTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
if maybeErr.IsFailure() {
- ctx.ResponseWriter.Reset() // this response is skipped because it's empty.
+ ctx.Recorder().Reset() // this response is skipped because it's empty.
}
return true
})
@@ -143,16 +143,20 @@ var TransientTransactionScope = TransactionScopeFunc(func(maybeErr TransactionEr
// is not written to the client, and an error status code is written instead.
var RequestTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
if maybeErr.IsFailure() {
+
// we need to register a beforeResponseFlush event here in order
// to execute last the EmitError
// (which will reset the whole response's body, status code and headers setted from normal flow or other transactions too)
ctx.ResponseWriter.SetBeforeFlush(func() {
+ // we need to re-take the context's response writer
+ // because inside here the response writer is changed to the original's
+ // look ~context:1306
+ w := ctx.ResponseWriter.(*ResponseRecorder)
if maybeErr.Reason != "" {
- ctx.ResponseWriter.Reset()
// send the error with the info user provided
- ctx.ResponseWriter.SetBodyString(maybeErr.Reason)
- ctx.ResponseWriter.WriteHeader(maybeErr.StatusCode)
- ctx.ResponseWriter.SetContentType(maybeErr.ContentType)
+ w.SetBodyString(maybeErr.Reason)
+ w.WriteHeader(maybeErr.StatusCode)
+ w.SetContentType(maybeErr.ContentType)
} else {
// else execute the registered user error and skip the next transactions and all normal flow,
ctx.EmitError(maybeErr.StatusCode)
diff --git a/webfs.go b/webfs.go
index 4f272d48..87427e19 100644
--- a/webfs.go
+++ b/webfs.go
@@ -131,12 +131,12 @@ func (w *webfs) Build() HandlerFunc {
}
w.handler = func(ctx *Context) {
- writer := ctx.ResponseWriter.ResponseWriter
+ writer := ctx.ResponseWriter
if w.gzip && ctx.clientAllowsGzip() {
ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
- gzipResWriter := acquireGzipResponseWriter(ctx.ResponseWriter.ResponseWriter)
+ gzipResWriter := acquireGzipResponseWriter(ctx.ResponseWriter) //.ResponseWriter)
writer = gzipResWriter
defer releaseGzipResponseWriter(gzipResWriter)
}