package iris import ( "bytes" "encoding/json" "encoding/xml" "fmt" "io" "io/ioutil" "mime/multipart" "net" "net/http" "os" "path" "reflect" "regexp" "runtime" "strconv" "strings" "time" "github.com/iris-contrib/formBinder" "github.com/kataras/go-errors" "github.com/kataras/go-fs" "github.com/kataras/go-sessions" ) const ( // ContentType represents the header["Content-Type"] contentType = "Content-Type" // ContentLength represents the header["Content-Length"] contentLength = "Content-Length" // contentEncodingHeader represents the header["Content-Encoding"] contentEncodingHeader = "Content-Encoding" // varyHeader represents the header "Vary" varyHeader = "Vary" // acceptEncodingHeader represents the header key & value "Accept-Encoding" acceptEncodingHeader = "Accept-Encoding" // ContentHTML is the string of text/html response headers contentHTML = "text/html" // ContentBinary header value for binary data. contentBinary = "application/octet-stream" // ContentJSON header value for JSON data. contentJSON = "application/json" // ContentJSONP header value for JSONP & Javascript data. contentJSONP = "application/javascript" // ContentJavascript header value for Javascript/JSONP // conversional contentJavascript = "application/javascript" // ContentText header value for Text data. contentText = "text/plain" // ContentXML header value for XML data. contentXML = "text/xml" // contentMarkdown custom key/content type, the real is the text/html contentMarkdown = "text/markdown" // LastModified "Last-Modified" lastModified = "Last-Modified" // IfModifiedSince "If-Modified-Since" ifModifiedSince = "If-Modified-Since" // ContentDisposition "Content-Disposition" contentDisposition = "Content-Disposition" // CacheControl "Cache-Control" cacheControl = "Cache-Control" // stopExecutionPosition used inside the Context, is the number which shows us that the context's middleware manually stop the execution stopExecutionPosition = 255 ) type ( requestValue struct { key []byte value interface{} } requestValues []requestValue ) func (r *requestValues) Set(key string, value interface{}) { args := *r n := len(args) for i := 0; i < n; i++ { kv := &args[i] if string(kv.key) == key { kv.value = value return } } c := cap(args) if c > n { args = args[:n+1] kv := &args[n] kv.key = append(kv.key[:0], key...) kv.value = value *r = args return } kv := requestValue{} kv.key = append(kv.key[:0], key...) kv.value = value *r = append(args, kv) } func (r *requestValues) Get(key string) interface{} { args := *r n := len(args) for i := 0; i < n; i++ { kv := &args[i] if string(kv.key) == key { return kv.value } } return nil } func (r *requestValues) Reset() { *r = (*r)[:0] } type ( // Map is just a conversion for a map[string]interface{} // should not be used inside Render when PongoEngine is used. Map map[string]interface{} // 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 by default, when record is on then *ResponseRecorder Request *http.Request values requestValues framework *Framework //keep track all registered middleware (handlers) Middleware Middleware // exported because is useful for debugging session sessions.Session // Pos is the position number of the Context, look .Next to understand Pos int // exported because is useful for debugging } ) // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------Handler(s) Execution------------------------------------ // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // Do calls the first handler only, it's like Next with negative pos, used only on Router&MemoryRouter func (ctx *Context) Do() { ctx.Pos = 0 ctx.Middleware[0].Serve(ctx) } // Next calls all the next handler from the middleware stack, it used inside a middleware func (ctx *Context) Next() { //set position to the next ctx.Pos++ //run the next if ctx.Pos < len(ctx.Middleware) { ctx.Middleware[ctx.Pos].Serve(ctx) } } // StopExecution just sets the .pos to 255 in order to not move to the next middlewares(if any) func (ctx *Context) StopExecution() { ctx.Pos = stopExecutionPosition } // IsStopped checks and returns true if the current position of the Context is 255, means that the StopExecution has called func (ctx *Context) IsStopped() bool { return ctx.Pos == stopExecutionPosition } // GetHandlerName as requested returns the stack-name of the function which the Middleware is setted from func (ctx *Context) GetHandlerName() string { return runtime.FuncForPC(reflect.ValueOf(ctx.Middleware[len(ctx.Middleware)-1]).Pointer()).Name() } // ExecRoute calls any route (mostly "offline" route) like it was requested by the user, but it is not. // Offline means that the route is registered to the iris and have all features that a normal route has // BUT it isn't available by browsing, its handlers executed only when other handler's context call them // it can validate paths, has sessions, path parameters and all. // // You can find the Route by iris.Lookup("theRouteName") // you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // // It doesn't changes the global state, if a route was "offline" it remains offline. // // see ExecRouteAgainst(routeName, againstRequestPath string), // iris.None(...) and iris.SetRouteOnline/SetRouteOffline // For more details look: https://github.com/kataras/iris/issues/585 // // Example: https://github.com/iris-contrib/examples/tree/master/route_state func (ctx *Context) ExecRoute(r Route) *Context { return ctx.ExecRouteAgainst(r, ctx.Path()) } // ExecRouteAgainst calls any iris.Route against a 'virtually' request path // like it was requested by the user, but it is not. // Offline means that the route is registered to the iris and have all features that a normal route has // BUT it isn't available by browsing, its handlers executed only when other handler's context call them // it can validate paths, has sessions, path parameters and all. // // You can find the Route by iris.Lookup("theRouteName") // you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // // It doesn't changes the global state, if a route was "offline" it remains offline. // // see ExecRoute(routeName), // iris.None(...) and iris.SetRouteOnline/SetRouteOffline // For more details look: https://github.com/kataras/iris/issues/585 // // Example: https://github.com/iris-contrib/examples/tree/master/route_state func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Context { if r != nil { context := &(*ctx) context.Middleware = context.Middleware[0:0] context.values.Reset() tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain()) tree.entry.get(againstRequestPath, context) if len(context.Middleware) > 0 { context.Do() return context } } return nil } // Prioritize is a middleware which executes a route against this path // when the request's Path has a prefix of the route's STATIC PART // is not executing ExecRoute to determinate if it's valid, for performance reasons // if this function is not enough for you and you want to test more than one parameterized path // then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ } // // You can find the Route by iris.Lookup("theRouteName") // you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // // if the route found then it executes that and don't continue to the next handler // if not found then continue to the next handler func Prioritize(r Route) HandlerFunc { if r != nil { return func(ctx *Context) { reqPath := ctx.Path() if strings.HasPrefix(reqPath, r.StaticPath()) { newctx := ctx.ExecRouteAgainst(r, reqPath) if newctx == nil { // route not found. ctx.EmitError(StatusNotFound) } return } // execute the next handler if no prefix // here look, the only error we catch is the 404, // we can't go ctx.Next() and believe that the next handler will manage the error // because it will not, we are not on the router. ctx.Next() } } return func(ctx *Context) { ctx.Next() } } // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------Request URL, Method, IP & Headers getters--------------- // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // Method returns the http request method // same as *http.Request.Method func (ctx *Context) Method() string { return ctx.Request.Method } // Host returns the host part of the current url func (ctx *Context) Host() string { h := ctx.Request.URL.Host if h == "" { h = ctx.Request.Host } return h } // ServerHost returns the server host taken by *http.Request.Host func (ctx *Context) ServerHost() string { return ctx.Request.Host } // Subdomain returns the subdomain (string) of this request, if any func (ctx *Context) Subdomain() (subdomain string) { host := ctx.Host() if index := strings.IndexByte(host, '.'); index > 0 { subdomain = host[0:index] } return } // VirtualHostname returns the hostname that user registers, host path maybe differs from the real which is HostString, which taken from a net.listener func (ctx *Context) VirtualHostname() string { realhost := ctx.Host() hostname := realhost virtualhost := ctx.framework.mux.hostname if portIdx := strings.IndexByte(hostname, ':'); portIdx > 0 { hostname = hostname[0:portIdx] } if idxDotAnd := strings.LastIndexByte(hostname, '.'); idxDotAnd > 0 { s := hostname[idxDotAnd:] // means that we have the request's host mymachine.com or 127.0.0.1/0.0.0.0, but for the second option we will need to replace it with the hostname that the dev was registered // this needed to parse correct the {{ url }} iris global template engine's function if s == ".1" { hostname = strings.Replace(hostname, "127.0.0.1", virtualhost, 1) } else if s == ".0" { hostname = strings.Replace(hostname, "0.0.0.0", virtualhost, 1) } // } else { hostname = strings.Replace(hostname, "localhost", virtualhost, 1) } return hostname } // Path returns the full escaped path as string // for unescaped use: ctx.RequestCtx.RequestURI() or RequestPath(escape bool) func (ctx *Context) Path() string { return ctx.RequestPath(ctx.framework.Config.EnablePathEscape) } // RequestPath returns the requested path func (ctx *Context) RequestPath(escape bool) string { if escape { // NOTE: for example: // DecodeURI decodes %2F to '/' // DecodeQuery decodes any %20 to whitespace // here we choose to be query-decoded only // and with context.ParamDecoded the user receives a URI decoded path parameter. // see https://github.com/iris-contrib/examples/tree/master/named_parameters_pathescape // and https://github.com/iris-contrib/examples/tree/master/pathescape return DecodeQuery(ctx.Request.URL.EscapedPath()) } return ctx.Request.URL.Path } // RemoteAddr tries to return the real client's request IP func (ctx *Context) RemoteAddr() string { header := ctx.RequestHeader("X-Real-Ip") realIP := strings.TrimSpace(header) if realIP != "" { return realIP } realIP = ctx.RequestHeader("X-Forwarded-For") idx := strings.IndexByte(realIP, ',') if idx >= 0 { realIP = realIP[0:idx] } realIP = strings.TrimSpace(realIP) if realIP != "" { return realIP } addr := strings.TrimSpace(ctx.Request.RemoteAddr) if len(addr) == 0 { return "" } // if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is if ip, _, err := net.SplitHostPort(addr); err == nil { return ip } return addr } // RequestHeader returns the request header's value // accepts one parameter, the key of the header (string) // returns string func (ctx *Context) RequestHeader(k string) string { return ctx.Request.Header.Get(k) } // IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest) // // Read more at: http://www.w3schools.com/ajax/ func (ctx *Context) IsAjax() bool { return ctx.RequestHeader("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" } // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------GET & POST arguments------------------------------------ // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // URLParam returns the get parameter from a request , if any func (ctx *Context) URLParam(key string) string { return ctx.Request.URL.Query().Get(key) } // URLParams returns a map of GET query parameters separated by comma if more than one // it returns an empty map if nothing founds func (ctx *Context) URLParams() map[string]string { values := map[string]string{} q := ctx.URLParamsAsMulti() if q != nil { for k, v := range q { values[k] = strings.Join(v, ",") } } return values } // URLParamsAsMulti returns a map of list contains the url get parameters func (ctx *Context) URLParamsAsMulti() map[string][]string { return ctx.Request.URL.Query() } // URLParamInt returns the url query parameter as int value from a request , returns error on parse fail func (ctx *Context) URLParamInt(key string) (int, error) { return strconv.Atoi(ctx.URLParam(key)) } // URLParamInt64 returns the url query parameter as int64 value from a request , returns error on parse fail func (ctx *Context) URLParamInt64(key string) (int64, error) { return strconv.ParseInt(ctx.URLParam(key), 10, 64) } func (ctx *Context) askParseForm() error { if ctx.Request.Form == nil { if err := ctx.Request.ParseForm(); err != nil { return err } } return nil } // FormValues returns all post data values with their keys // form data, get, post & put query arguments // // NOTE: A check for nil is necessary for zero results func (ctx *Context) FormValues() map[string][]string { // we skip the check of multipart form, takes too much memory, if user wants it can do manually now. if err := ctx.askParseForm(); err != nil { return nil } return ctx.Request.Form // nothing more to do, it's already contains both query and post & put args. } // FormValue returns a single form value by its name/key func (ctx *Context) FormValue(name string) string { return ctx.Request.FormValue(name) } // PostValue returns a form's only-post value by its name // same as Request.PostFormValue func (ctx *Context) PostValue(name string) string { return ctx.Request.PostFormValue(name) } // FormFile returns the first file for the provided form key. // FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary. // // same as Request.FormFile func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) { return ctx.Request.FormFile(key) } var ( errTemplateExecute = errors.New("Unable to execute a template. Trace: %s") errReadBody = errors.New("While trying to read %s from the request body. Trace %s") errServeContent = errors.New("While trying to serve content to the client. Trace %s") ) // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------Request Body Binders/Readers---------------------------- // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // NOTE: No default max body size http package has some built'n protection for DoS attacks // See iris.Config.MaxBytesReader, https://github.com/golang/go/issues/2093#issuecomment-66057813 // and https://github.com/golang/go/issues/2093#issuecomment-66057824 // LimitRequestBodySize is a middleware which sets a request body size limit for all next handlers // should be registered before all other handlers var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) HandlerFunc { return func(ctx *Context) { ctx.SetMaxRequestBodySize(maxRequestBodySizeBytes) ctx.Next() } } // SetMaxRequestBodySize sets a limit to the request body size // should be called before reading the request body from the client func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) { ctx.Request.Body = http.MaxBytesReader(ctx.ResponseWriter, ctx.Request.Body, limitOverBytes) } // BodyDecoder is an interface which any struct can implement in order to customize the decode action // from ReadJSON and ReadXML // // Trivial example of this could be: // type User struct { Username string } // // func (u *User) Decode(data []byte) error { // return json.Unmarshal(data, u) // } // // the 'context.ReadJSON/ReadXML(&User{})' will call the User's // Decode option to decode the request body // // Note: This is totally optionally, the default decoders // for ReadJSON is the encoding/json and for ReadXML is the encoding/xml type BodyDecoder interface { Decode(data []byte) error } // Unmarshaler is the interface implemented by types that can unmarshal any raw data // TIP INFO: Any v object which implements the BodyDecoder can be override the unmarshaler type Unmarshaler interface { Unmarshal(data []byte, v interface{}) error } // UnmarshalerFunc a shortcut for the Unmarshaler interface // // See 'Unmarshaler' and 'BodyDecoder' for more type UnmarshalerFunc func(data []byte, v interface{}) error // Unmarshal parses the X-encoded data and stores the result in the value pointed to by v. // Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps, // slices, and pointers as necessary. func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error { return u(data, v) } // UnmarshalBody reads the request's body and binds it to a value or pointer of any type // Examples of usage: context.ReadJSON, context.ReadXML func (ctx *Context) UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error { if ctx.Request.Body == nil { return errors.New("Empty body, please send request body!") } rawData, err := ioutil.ReadAll(ctx.Request.Body) if err != nil { return err } if ctx.framework.Config.DisableBodyConsumptionOnUnmarshal { // * remember, Request.Body has no Bytes(), we have to consume them first // and after re-set them to the body, this is the only solution. ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(rawData)) } // check if the v contains its own decode // in this case the v should be a pointer also, // but this is up to the user's custom Decode implementation* // // See 'BodyDecoder' for more if decoder, isDecoder := v.(BodyDecoder); isDecoder { return decoder.Decode(rawData) } // check if v is already a pointer, if yes then pass as it's if reflect.TypeOf(v).Kind() == reflect.Ptr { return unmarshaler.Unmarshal(rawData, v) } // finally, if the v doesn't contains a self-body decoder and it's not a pointer // use the custom unmarshaler to bind the body return unmarshaler.Unmarshal(rawData, &v) } // ReadJSON reads JSON from request's body and binds it to a value of any json-valid type func (ctx *Context) ReadJSON(jsonObject interface{}) error { return ctx.UnmarshalBody(jsonObject, UnmarshalerFunc(json.Unmarshal)) } // ReadXML reads XML from request's body and binds it to a value of any xml-valid type func (ctx *Context) ReadXML(xmlObject interface{}) error { return ctx.UnmarshalBody(xmlObject, UnmarshalerFunc(xml.Unmarshal)) } // ReadForm binds the formObject with the form data // it supports any kind of struct func (ctx *Context) ReadForm(formObject interface{}) error { values := ctx.FormValues() if values == nil { return errors.New("An empty form passed on context.ReadForm") } return errReadBody.With(formBinder.Decode(values, formObject)) } /* Response */ // SetContentType sets the response writer's header key 'Content-Type' to a given value(s) func (ctx *Context) SetContentType(s string) { ctx.ResponseWriter.Header().Set(contentType, s) } // SetHeader write to the response writer's header to a given key the given value func (ctx *Context) SetHeader(k string, v string) { ctx.ResponseWriter.Header().Add(k, v) } // SetStatusCode sets the status code header to the response // // same as .WriteHeader, iris takes cares of your status code seriously func (ctx *Context) SetStatusCode(statusCode int) { ctx.ResponseWriter.WriteHeader(statusCode) } // Redirect redirect sends a redirect response the client // accepts 2 parameters string and an optional int // first parameter is the url to redirect // second parameter is the http status should send, default is 302 (StatusFound), // you can set it to 301 (Permant redirect), if that's nessecery func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) { ctx.StopExecution() httpStatus := StatusFound // a 'temporary-redirect-like' which works better than for our purpose if statusHeader != nil && len(statusHeader) > 0 && statusHeader[0] > 0 { httpStatus = statusHeader[0] } if urlToRedirect == ctx.Path() { if ctx.framework.Config.IsDevelopment { ctx.Log("Trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect) } } 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 func (ctx *Context) RedirectTo(routeName string, args ...interface{}) { s := ctx.framework.URL(routeName, args...) if s != "" { ctx.Redirect(s, StatusFound) } } // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------(Custom) Errors----------------------------------------- // ----------------------Look iris.OnError/EmitError for more--------------------------- // ------------------------------------------------------------------------------------- // NotFound emits an error 404 to the client, using the custom http errors // if no custom errors provided then it sends the default error message func (ctx *Context) NotFound() { ctx.framework.EmitError(StatusNotFound, ctx) } // Panic emits an error 500 to the client, using the custom http errors // if no custom errors rpovided then it sends the default error message func (ctx *Context) Panic() { ctx.framework.EmitError(StatusInternalServerError, ctx) } // EmitError executes the custom error by the http status code passed to the function func (ctx *Context) EmitError(statusCode int) { ctx.framework.EmitError(statusCode, ctx) ctx.StopExecution() } // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -------------------------Context's gzip inline response writer ---------------------- // ---------------------Look template.go & iris.go for more options--------------------- // ------------------------------------------------------------------------------------- var ( errClientDoesNotSupportGzip = errors.New("Client doesn't supports gzip compression") ) func (ctx *Context) clientAllowsGzip() bool { if h := ctx.RequestHeader(acceptEncodingHeader); h != "" { for _, v := range strings.Split(h, ";") { if strings.Contains(v, "gzip") { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){ return true } } } return false } // WriteGzip accepts bytes, which are compressed to gzip format and sent to the client. // returns the number of bytes written and an error ( if the client doesn' supports gzip compression) func (ctx *Context) WriteGzip(b []byte) (int, error) { if ctx.clientAllowsGzip() { ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader) gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter) n, err := gzipWriter.Write(b) fs.ReleaseGzipWriter(gzipWriter) if err == nil { ctx.SetHeader(contentEncodingHeader, "gzip") } // else write the contents as it is? no let's create a new func for this return n, err } return 0, errClientDoesNotSupportGzip } // TryWriteGzip accepts bytes, which are compressed to gzip format and sent to the client. // If client does not supprots gzip then the contents are written as they are, uncompressed. func (ctx *Context) TryWriteGzip(b []byte) (int, error) { n, err := ctx.WriteGzip(b) if err != nil { // check if the error came from gzip not allowed and not the writer itself if _, ok := err.(*errors.Error); ok { // client didn't supported gzip, write them uncompressed: return ctx.ResponseWriter.Write(b) } } return n, err } // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // -----------------------------Render and powerful content negotiation----------------- // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- // renderSerialized renders contents with a serializer with status OK which you can change using RenderWithStatus or ctx.SetStatusCode(iris.StatusCode) func (ctx *Context) renderSerialized(contentType string, obj interface{}, options ...map[string]interface{}) error { s := ctx.framework.serializers finalResult, err := s.Serialize(contentType, obj, options...) if err != nil { return err } gzipEnabled := ctx.framework.Config.Gzip charset := ctx.framework.Config.Charset if len(options) > 0 { gzipEnabled = getGzipOption(gzipEnabled, options[0]) // located to the template.go below the RenderOptions charset = getCharsetOption(charset, options[0]) } ctype := contentType if ctype == contentMarkdown { // remember the text/markdown is just a custom internal iris content type, which in reallity renders html ctype = contentHTML } if ctype != contentBinary { // set the charset only on non-binary data ctype += "; charset=" + charset } ctx.SetContentType(ctype) if gzipEnabled { ctx.TryWriteGzip(finalResult) } else { ctx.ResponseWriter.Write(finalResult) } ctx.SetStatusCode(StatusOK) return nil } // RenderTemplateSource serves a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string func (ctx *Context) RenderTemplateSource(status int, src string, binding interface{}, options ...map[string]interface{}) error { err := ctx.framework.templates.renderSource(ctx, src, binding, options...) if err == nil { ctx.SetStatusCode(status) } return err } // 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...) } // 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 } // Render same as .RenderWithStatus but with status to iris.StatusOK (200) if no previous status exists // 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 engine func (ctx *Context) Render(name string, binding interface{}, options ...map[string]interface{}) error { errCode := ctx.ResponseWriter.StatusCode() if errCode <= 0 { errCode = StatusOK } return ctx.RenderWithStatus(errCode, name, binding, options...) } // MustRender same as .Render but returns 503 service unavailable http status with a (html) message if render failed // Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engine func (ctx *Context) MustRender(name string, binding interface{}, options ...map[string]interface{}) { if err := ctx.Render(name, binding, options...); err != nil { ctx.HTML(StatusServiceUnavailable, fmt.Sprintf("