From 6f594c0a7c641cc98bd683163fffbf5fa5fc8de6 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 25 Jan 2018 03:16:49 +0200 Subject: [PATCH] add vscode extension link and badge | Some internal improvements (not completed yet) --- FAQ.md | 10 +- README.md | 2 +- README_GR.md | 2 +- README_RU.md | 2 +- README_ZH.md | 2 +- .../casbin/wrapper/main_test.go | 4 +- _examples/experimental-handlers/csrf/main.go | 2 +- _examples/subdomains/redirect/main_test.go | 5 +- _examples/subdomains/www/main.go | 14 +- _examples/subdomains/www/main_test.go | 4 +- context/context.go | 168 +++++++++++++----- core/router/api_builder.go | 78 ++++---- core/router/fs.go | 167 +++++++---------- core/router/party.go | 18 +- hero/func_result_test.go | 23 +++ httptest/httptest.go | 7 +- 16 files changed, 291 insertions(+), 217 deletions(-) diff --git a/FAQ.md b/FAQ.md index 9504063090..63d89d15f8 100644 --- a/FAQ.md +++ b/FAQ.md @@ -10,6 +10,14 @@ Add a `badge` to your open-source projects powered by [Iris](https://iris-go.com > The badge is optionally, of course, it is just a simple and fast way to support Iris. The badge is work of a third-party, taken from https://github.com/blob-go/blob-go which was published by our friend @clover113 and we loved it<3 +## Editors & IDEs Extensions + +### Visual Studio Code + + + +> Please feel free to list your own Iris extension(s) here by [PR](https://github.com/kataras/iris/pulls) + ## How to upgrade ```sh @@ -18,7 +26,7 @@ go get -u github.com/kataras/iris ## Learning -More than 50 practical examples, tutorials and articles at: +More than 100 practical examples, tutorials and articles at: - https://github.com/kataras/iris/tree/master/_examples - https://github.com/iris-contrib/examples diff --git a/README.md b/README.md index 45aa4116e3..fdaf6181e0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris is a fast, simple yet fully featured and very efficient web framework for Go. diff --git a/README_GR.md b/README_GR.md index cc6d0e1415..8eb17c9b66 100644 --- a/README_GR.md +++ b/README_GR.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go. diff --git a/README_RU.md b/README_RU.md index 85715d8196..520cf0ca5d 100644 --- a/README_RU.md +++ b/README_RU.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go. diff --git a/README_ZH.md b/README_ZH.md index ec21ab48d8..bd58ada60f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://github.com/kataras/vscode-iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。 diff --git a/_examples/experimental-handlers/casbin/wrapper/main_test.go b/_examples/experimental-handlers/casbin/wrapper/main_test.go index b107bfda71..152a810835 100644 --- a/_examples/experimental-handlers/casbin/wrapper/main_test.go +++ b/_examples/experimental-handlers/casbin/wrapper/main_test.go @@ -9,7 +9,7 @@ import ( func TestCasbinWrapper(t *testing.T) { app := newApp() - e := httptest.New(t, app, httptest.Debug(true)) + e := httptest.New(t, app) type ttcasbin struct { username string @@ -43,7 +43,6 @@ func TestCasbinWrapper(t *testing.T) { check(e, tt.method, tt.path, tt.username, tt.status) } - println("ADMIN ROLES") ttAdmin := []ttcasbin{ {"cathrin", "/dataset1/item", "GET", 200}, {"cathrin", "/dataset1/item", "POST", 200}, @@ -57,7 +56,6 @@ func TestCasbinWrapper(t *testing.T) { check(e, tt.method, tt.path, tt.username, tt.status) } - println("ADMIN ROLE FOR cathrin DELETED") Enforcer.DeleteRolesForUser("cathrin") ttAdminDeleted := []ttcasbin{ diff --git a/_examples/experimental-handlers/csrf/main.go b/_examples/experimental-handlers/csrf/main.go index 3e2f27de03..c9e6a7d58d 100644 --- a/_examples/experimental-handlers/csrf/main.go +++ b/_examples/experimental-handlers/csrf/main.go @@ -42,7 +42,7 @@ func getSignupForm(ctx iris.Context) { // views/signup.html just needs a {{ .csrfField }} template tag for // csrf.TemplateField to inject the CSRF token into. Easy! ctx.ViewData(csrf.TemplateTag, csrf.TemplateField(ctx)) - ctx.View("views/user/signup.html") + ctx.View("user/signup.html") // We could also retrieve the token directly from csrf.Token(r) and // set it in the request header - ctx.GetHeader("X-CSRF-Token", token) diff --git a/_examples/subdomains/redirect/main_test.go b/_examples/subdomains/redirect/main_test.go index cc4f931d2a..4f2a62cf27 100644 --- a/_examples/subdomains/redirect/main_test.go +++ b/_examples/subdomains/redirect/main_test.go @@ -24,10 +24,7 @@ func TestSubdomainRedirectWWW(t *testing.T) { } for _, test := range tests { - req := e.GET(test.path) - // req.WithURL("http://www." + root) - - req.Expect().Status(httptest.StatusOK).Body().Equal(test.response) + e.GET(test.path).Expect().Status(httptest.StatusOK).Body().Equal(test.response) } } diff --git a/_examples/subdomains/www/main.go b/_examples/subdomains/www/main.go index 26dc267213..c41e343d52 100644 --- a/_examples/subdomains/www/main.go +++ b/_examples/subdomains/www/main.go @@ -30,19 +30,19 @@ func newApp() *iris.Application { www := app.Party("www.") { - // http://www.mydomain.com/hi - www.Get("/hi", func(ctx iris.Context) { - ctx.Writef("hi from www.mydomain.com") - }) - // Just to show how you can get all routes and copy them to another // party or subdomain: - // Get all routes that are registered so far, including all "Parties" but subdomains: + // Get all routes that are registered so far, including all "Parties" and subdomains: currentRoutes := app.GetRoutes() // Register them to the www subdomain/vhost as well: for _, r := range currentRoutes { - www.Handle(r.Method, r.Path, r.Handlers...) + www.Handle(r.Method, r.Tmpl().Src, r.Handlers...) } + + // http://www.mydomain.com/hi + www.Get("/hi", func(ctx iris.Context) { + ctx.Writef("hi from www.mydomain.com") + }) } // See also the "subdomains/redirect" to register redirect router wrappers between subdomains, // i.e mydomain.com to www.mydomain.com (like facebook does for SEO reasons(;)). diff --git a/_examples/subdomains/www/main_test.go b/_examples/subdomains/www/main_test.go index f12c6bb86c..cd698384ec 100644 --- a/_examples/subdomains/www/main_test.go +++ b/_examples/subdomains/www/main_test.go @@ -41,13 +41,13 @@ func TestSubdomainWWW(t *testing.T) { } host := "localhost:1111" - e := httptest.New(t, app, httptest.URL("http://"+host)) + e := httptest.New(t, app, httptest.URL("http://"+host), httptest.Debug(false)) for _, test := range tests { req := e.Request(test.method, test.path) if subdomain := test.subdomain; subdomain != "" { - req.WithURL("http://" + subdomain + "." + host) + req = req.WithURL("http://" + subdomain + "." + host) } req.Expect(). diff --git a/context/context.go b/context/context.go index a776143bf5..ed58bda743 100644 --- a/context/context.go +++ b/context/context.go @@ -891,18 +891,6 @@ type Context interface { var _ Context = (*context)(nil) -// Next calls all the next handler from the handlers chain, -// it should be used inside a middleware. -func Next(ctx Context) { - if ctx.IsStopped() { - return - } - if n, handlers := ctx.HandlerIndex(-1)+1, ctx.Handlers(); n < len(handlers) { - ctx.HandlerIndex(n) - handlers[n](ctx) - } -} - // Do calls the SetHandlers(handlers) // and executes the first handler, // handlers should not be empty. @@ -1159,17 +1147,38 @@ func (ctx *context) HandlerName() string { return HandlerName(ctx.handlers[ctx.currentHandlerIndex]) } -// Do sets the handler index to zero, executes the first handler -// and the rest of the Handlers if ctx.Next() was called. -// func (ctx *context) Do() { -// ctx.currentHandlerIndex = 0 -// ctx.handlers[0](ctx) // it calls this *context -// } // -> replaced with inline on router.go +// Next is the function that executed when `ctx.Next()` is called. +// It can be changed to a customized one if needed (very advanced usage). +// +// See `DefaultNext` for more information about this and why it's exported like this. +var Next = DefaultNext ///TODO: add an example for this usecase, i.e describe handlers and skip only file handlers. + +// DefaultNext is the default function that executed on each middleware if `ctx.Next()` +// is called. +// +// DefaultNext calls the next handler from the handlers chain by registration order, +// it should be used inside a middleware. +// +// It can be changed to a customized one if needed (very advanced usage). +// +// Developers are free to customize the whole or part of the Context's implementation +// by implementing a new `context.Context` (see https://github.com/kataras/iris/tree/master/_examples/routing/custom-context) +// or by just override the `context.Next` package-level field, `context.DefaultNext` is exported +// in order to be able for developers to merge your customized version one with the default behavior as well. +func DefaultNext(ctx Context) { + if ctx.IsStopped() { + return + } + if n, handlers := ctx.HandlerIndex(-1)+1, ctx.Handlers(); n < len(handlers) { + ctx.HandlerIndex(n) + handlers[n](ctx) + } +} // Next calls all the next handler from the handlers chain, // it should be used inside a middleware. // -// Note: Custom context should override this method in order to be able to pass its own context.context implementation. +// Note: Custom context should override this method in order to be able to pass its own context.Context implementation. func (ctx *context) Next() { // or context.Next(ctx) Next(ctx) } @@ -2046,29 +2055,111 @@ var ( varyHeaderKey = "Vary" ) -// staticCachePassed checks the IfModifiedSince header and -// returns true if (client-side) duration has expired -func (ctx *context) staticCachePassed(modtime time.Time) bool { - if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && modtime.Before(t.Add(StaticCacheDuration)) { - ctx.writer.Header().Del(contentTypeHeaderKey) - ctx.writer.Header().Del(contentLengthHeaderKey) - ctx.StatusCode(http.StatusNotModified) - return true +var unixEpochTime = time.Unix(0, 0) + +// IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). +func IsZeroTime(t time.Time) bool { + return t.IsZero() || t.Equal(unixEpochTime) +} + +// ParseTime parses a time header (such as the Date: header), +// trying each forth formats (or three if Application's configuration's TimeFormat is defaulted) +// that are allowed by HTTP/1.1: +// Application's configuration's TimeFormat or/and http.TimeFormat, +// time.RFC850, and time.ANSIC. +// +// Look `context#FormatTime` for the opossite operation (Time to string). +var ParseTime = func(ctx Context, text string) (t time.Time, err error) { + t, err = time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), text) + if err != nil { + return http.ParseTime(text) } - return false + + return +} + +// FormatTime returns a textual representation of the time value formatted +// according to the Application's configuration's TimeFormat field +// which defines the format. +// +// Look `context#ParseTime` for the opossite operation (string to Time). +var FormatTime = func(ctx Context, t time.Time) string { + return t.Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) +} + +// SetLastModified sets the "Last-Modified" based on the "modtime" input. +// If "modtime" is zero then it does nothing. +// +// It's mostly internally on core/router and context packages. +func SetLastModified(ctx Context, modtime time.Time) { + if !IsZeroTime(modtime) { + ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime)) // or modtime.UTC()? + } +} + +// CheckIfModifiedSince checks if the response is modified since the "modtime". +// Note that it has nothing to do with server-side caching. +// It does those checks by checking if the "If-Modified-Since" request header +// sent by client or a previous server response header +// (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.) +// is a valid one and it's before the "modtime". +// +// A check for !modtime && err == nil is necessary to make sure that +// it's not modified since, because it may return false but without even +// had the chance to check the client-side (request) header due to some errors, +// like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero +// or if parsing time from the header failed. +// +// It's mostly used internally, e.g. `context#WriteWithExpiration`. +func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) { + if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead { + return false, errors.New("skip: method") + } + ims := ctx.GetHeader(ifModifiedSinceHeaderKey) + if ims == "" || IsZeroTime(modtime) { + return false, errors.New("skip: zero time") + } + t, err := ParseTime(ctx, ims) + if err != nil { + return false, errors.New("skip: " + err.Error()) + } + // sub-second precision, so + // use mtime < t+1s instead of mtime <= t to check for unmodified. + if modtime.Before(t.Add(1 * time.Second)) { + return false, nil + } + return true, nil +} + +// WriteNotModified sends a 304 "Not Modified" status code to the client, +// it makes sure that the content type, the content length headers +// and any "ETag" are removed before the response sent. +// +// It's mostly used internally on core/router/fs.go and context methods. +func WriteNotModified(ctx Context) { + // RFC 7232 section 4.1: + // a sender SHOULD NOT generate representation metadata other than the + // above listed fields unless said metadata exists for the purpose of + // guiding cache updates (e.g.," Last-Modified" might be useful if the + // response does not have an ETag field). + h := ctx.ResponseWriter().Header() + delete(h, contentTypeHeaderKey) + delete(h, contentLengthHeaderKey) + if h.Get("Etag") != "" { + delete(h, lastModifiedHeaderKey) + } + ctx.StatusCode(http.StatusNotModified) } // WriteWithExpiration like Write but it sends with an expiration datetime // which is refreshed every package-level `StaticCacheDuration` field. func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) { - - if ctx.staticCachePassed(modtime) { + if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { + WriteNotModified(ctx) return 0, nil } - modtimeFormatted := modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) - ctx.Header(lastModifiedHeaderKey, modtimeFormatted) - + SetLastModified(ctx, modtime) return ctx.writer.Write(body) } @@ -2658,16 +2749,13 @@ const ( // You can define your own "Content-Type" header also, after this function call // Doesn't implements resuming (by range), use ctx.SendFile instead func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { - if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && modtime.Before(t.Add(1*time.Second)) { - ctx.writer.Header().Del(contentTypeHeaderKey) - ctx.writer.Header().Del(contentLengthHeaderKey) - ctx.StatusCode(http.StatusNotModified) + if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil { + WriteNotModified(ctx) return nil } ctx.ContentType(filename) - ctx.writer.Header().Set(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) - ctx.StatusCode(http.StatusOK) + SetLastModified(ctx, modtime) var out io.Writer if gzipCompression && ctx.ClientSupportsGzip() { ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey) @@ -2680,7 +2768,7 @@ func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime out = ctx.writer } _, err := io.Copy(out, content) - return errServeContent.With(err) + return errServeContent.With(err) ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api? } // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 4fb3bb4646..5864aa1e56 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -629,7 +629,6 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { return api.Favicon(path.Join(favPath, "favicon.ico")) } - cType := TypeByFilename(favPath) // copy the bytes here in order to cache and not read the ico on each request. cacheFav := make([]byte, fi.Size()) if _, err = f.Read(cacheFav); err != nil { @@ -641,25 +640,14 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { Format(favPath, "favicon: couldn't read the data bytes for file: "+err.Error())) return nil } - modtime := "" - h := func(ctx context.Context) { - if modtime == "" { - modtime = fi.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()) - } - if t, err := time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), ctx.GetHeader(ifModifiedSinceHeaderKey)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) { - - ctx.ResponseWriter().Header().Del(contentTypeHeaderKey) - ctx.ResponseWriter().Header().Del(contentLengthHeaderKey) - ctx.StatusCode(http.StatusNotModified) - return - } - ctx.ResponseWriter().Header().Set(contentTypeHeaderKey, cType) - ctx.ResponseWriter().Header().Set(lastModifiedHeaderKey, modtime) - ctx.StatusCode(http.StatusOK) - if _, err := ctx.Write(cacheFav); err != nil { - // ctx.Application().Logger().Infof("error while trying to serve the favicon: %s", err.Error()) + modtime := time.Now() + cType := TypeByFilename(favPath) + h := func(ctx context.Context) { + ctx.ContentType(cType) + if _, err := ctx.WriteWithExpiration(cacheFav, modtime); err != nil { ctx.StatusCode(http.StatusInternalServerError) + ctx.Application().Logger().Debugf("while trying to serve the favicon: %s", err.Error()) } } @@ -698,16 +686,16 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { handler := func(ctx context.Context) { h(ctx) - if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 { - // re-check the content type here for any case, - // although the new code does it automatically but it's good to have it here. - if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists { - if fname := ctx.Params().Get(paramName); fname != "" { - cType := TypeByFilename(fname) - ctx.ContentType(cType) - } - } - } + // if ctx.GetStatusCode() >= 200 && ctx.GetStatusCode() < 400 { + // // re-check the content type here for any case, + // // although the new code does it automatically but it's good to have it here. + // if _, exists := ctx.ResponseWriter().Header()["Content-Type"]; !exists { + // if fname := ctx.Params().Get(paramName); fname != "" { + // cType := TypeByFilename(fname) + // ctx.ContentType(cType) + // } + // } + // } } requestPath = joinPath(requestPath, WildcardParam(paramName)) @@ -791,16 +779,20 @@ func (api *APIBuilder) FireErrorCode(ctx context.Context) { api.errorCodeHandlers.Fire(ctx) } -// Layout oerrides the parent template layout with a more specific layout for this Party -// returns this Party, to continue as normal +// Layout overrides the parent template layout with a more specific layout for this Party. +// It returns the current Party. +// +// The "tmplLayoutFile" should be a relative path to the templates dir. // Usage: +// // app := iris.New() +// app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension")) // my := app.Party("/my").Layout("layouts/mylayout.html") -// { -// my.Get("/", func(ctx context.Context) { -// ctx.MustRender("page1.html", nil) -// }) -// } +// my.Get("/", func(ctx iris.Context) { +// ctx.View("page1.html") +// }) +// +// Examples: https://github.com/kataras/iris/tree/master/_examples/view func (api *APIBuilder) Layout(tmplLayoutFile string) Party { api.Use(func(ctx context.Context) { ctx.ViewLayout(tmplLayoutFile) @@ -811,14 +803,14 @@ func (api *APIBuilder) Layout(tmplLayoutFile string) Party { } // joinHandlers uses to create a copy of all Handlers and return them in order to use inside the node -func joinHandlers(Handlers1 context.Handlers, Handlers2 context.Handlers) context.Handlers { - nowLen := len(Handlers1) - totalLen := nowLen + len(Handlers2) - // create a new slice of Handlers in order to store all handlers, the already handlers(Handlers) and the new +func joinHandlers(h1 context.Handlers, h2 context.Handlers) context.Handlers { + nowLen := len(h1) + totalLen := nowLen + len(h2) + // create a new slice of Handlers in order to merge the "h1" and "h2" newHandlers := make(context.Handlers, totalLen) - //copy the already Handlers to the just created - copy(newHandlers, Handlers1) - //start from there we finish, and store the new Handlers too - copy(newHandlers[nowLen:], Handlers2) + // copy the already Handlers to the just created + copy(newHandlers, h1) + // start from there we finish, and store the new Handlers too + copy(newHandlers[nowLen:], h2) return newHandlers } diff --git a/core/router/fs.go b/core/router/fs.go index 02f65822cc..822058cf8e 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -411,7 +411,7 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS // content must be seeked to the beginning of the file. // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ { - setLastModified(ctx, modtime) + context.SetLastModified(ctx, modtime) done, rangeReq := checkPreconditions(ctx, modtime) if done { return "", http.StatusNotModified @@ -515,6 +515,17 @@ func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc return "", code } +func etagEmptyOrStrongMatch(rangeValue string, etagValue string) bool { + etag, _ := scanETag(rangeValue) + if etag != "" { + if etagStrongMatch(etag, etagValue) { + return true + } + return false + } + return true +} + // scanETag determines if a syntactically valid ETag is present at s. If so, // the ETag and remaining text after consuming ETag is returned. Otherwise, // it returns "", "". @@ -595,22 +606,6 @@ func checkIfMatch(ctx context.Context) condResult { return condFalse } -func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult { - ius := ctx.GetHeader("If-Unmodified-Since") - if ius == "" || isZeroTime(modtime) { - return condNone - } - if t, err := http.ParseTime(ius); err == nil { - // The Date-Modified header truncates sub-second precision, so - // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { - return condTrue - } - return condFalse - } - return condNone -} - func checkIfNoneMatch(ctx context.Context) condResult { inm := ctx.GetHeader("If-None-Match") if inm == "" { @@ -640,86 +635,6 @@ func checkIfNoneMatch(ctx context.Context) condResult { return condTrue } -func checkIfModifiedSince(ctx context.Context, modtime time.Time) condResult { - if ctx.Method() != http.MethodGet && ctx.Method() != http.MethodHead { - return condNone - } - ims := ctx.GetHeader("If-Modified-Since") - if ims == "" || isZeroTime(modtime) { - return condNone - } - t, err := http.ParseTime(ims) - if err != nil { - return condNone - } - // The Date-Modified header truncates sub-second precision, so - // use mtime < t+1s instead of mtime <= t to check for unmodified. - if modtime.Before(t.Add(1 * time.Second)) { - return condFalse - } - return condTrue -} - -func checkIfRange(ctx context.Context, modtime time.Time) condResult { - if ctx.Method() != http.MethodGet { - return condNone - } - ir := ctx.GetHeader("If-Range") - if ir == "" { - return condNone - } - etag, _ := scanETag(ir) - if etag != "" { - if etagStrongMatch(etag, ctx.ResponseWriter().Header().Get("Etag")) { - return condTrue - } - return condFalse - - } - // The If-Range value is typically the ETag value, but it may also be - // the modtime date. See golang.org/issue/8367. - if modtime.IsZero() { - return condFalse - } - t, err := http.ParseTime(ir) - if err != nil { - return condFalse - } - if t.Unix() == modtime.Unix() { - return condTrue - } - return condFalse -} - -var unixEpochTime = time.Unix(0, 0) - -// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0). -func isZeroTime(t time.Time) bool { - return t.IsZero() || t.Equal(unixEpochTime) -} - -func setLastModified(ctx context.Context, modtime time.Time) { - if !isZeroTime(modtime) { - ctx.Header(lastModifiedHeaderKey, modtime.UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) - } -} - -func writeNotModified(ctx context.Context) { - // RFC 7232 section 4.1: - // a sender SHOULD NOT generate representation metadata other than the - // above listed fields unless said metadata exists for the purpose of - // guiding cache updates (e.g., Last-Modified might be useful if the - // response does not have an ETag field). - h := ctx.ResponseWriter().Header() - delete(h, contentTypeHeaderKey) - - delete(h, contentLengthHeaderKey) - if h.Get("Etag") != "" { - delete(h, "Last-Modified") - } - ctx.StatusCode(http.StatusNotModified) -} - // checkPreconditions evaluates request preconditions and reports whether a precondition // resulted in sending StatusNotModified or StatusPreconditionFailed. func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rangeHeader string) { @@ -736,28 +651,72 @@ func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rang switch checkIfNoneMatch(ctx) { case condFalse: if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead { - writeNotModified(ctx) + context.WriteNotModified(ctx) return true, "" } ctx.StatusCode(http.StatusPreconditionFailed) return true, "" case condNone: - if checkIfModifiedSince(ctx, modtime) == condFalse { - writeNotModified(ctx) + if modified, err := context.CheckIfModifiedSince(ctx, modtime); !modified && err == nil { + context.WriteNotModified(ctx) return true, "" } } rangeHeader = ctx.GetHeader("Range") if rangeHeader != "" { - if checkIfRange(ctx, modtime) == condFalse { + if checkIfRange(ctx, etagEmptyOrStrongMatch, modtime) == condFalse { rangeHeader = "" } } return false, rangeHeader } +func checkIfUnmodifiedSince(ctx context.Context, modtime time.Time) condResult { + ius := ctx.GetHeader("If-Unmodified-Since") + if ius == "" || context.IsZeroTime(modtime) { + return condNone + } + if t, err := context.ParseTime(ctx, ius); err == nil { + // The Date-Modified header truncates sub-second precision, so + // use mtime < t+1s instead of mtime <= t to check for unmodified. + if modtime.Before(t.Add(1 * time.Second)) { + return condTrue + } + return condFalse + } + return condNone +} + +func checkIfRange(ctx context.Context, etagEmptyOrStrongMatch func(ifRangeValue string, etagValue string) bool, modtime time.Time) condResult { + if ctx.Method() != http.MethodGet { + return condNone + } + ir := ctx.GetHeader("If-Range") + if ir == "" { + return condNone + } + + if etagEmptyOrStrongMatch(ir, ctx.GetHeader("Etag")) { + return condTrue + } + + // The If-Range value is typically the ETag value, but it may also be + // the modtime date. See golang.org/issue/8367. + if modtime.IsZero() { + return condFalse + } + t, err := context.ParseTime(ctx, ir) + if err != nil { + return condFalse + } + if t.Unix() == modtime.Unix() { + return condTrue + } + return condFalse +} + // name is '/'-separated, not filepath.Separator. func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) { const indexPage = "/index.html" @@ -826,8 +785,8 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo if !showList { return "", http.StatusForbidden } - if checkIfModifiedSince(ctx, d.ModTime()) == condFalse { - writeNotModified(ctx) + if modified, err := context.CheckIfModifiedSince(ctx, d.ModTime()); !modified && err == nil { + context.WriteNotModified(ctx) return "", http.StatusNotModified } ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())) @@ -842,7 +801,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo } // else, set the last modified as "serveContent" does. - setLastModified(ctx, d.ModTime()) + context.SetLastModified(ctx, d.ModTime()) // write the file to the response writer. contents, err := ioutil.ReadAll(f) diff --git a/core/router/party.go b/core/router/party.go index 9f84053969..a8145f9113 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -211,15 +211,19 @@ type Party interface { // Returns the GET *Route. StaticWeb(requestPath string, systemPath string) *Route - // Layout oerrides the parent template layout with a more specific layout for this Party - // returns this Party, to continue as normal + // Layout overrides the parent template layout with a more specific layout for this Party. + // It returns the current Party. + // + // The "tmplLayoutFile" should be a relative path to the templates dir. // Usage: + // // app := iris.New() + // app.RegisterView(iris.$VIEW_ENGINE("./views", ".$extension")) // my := app.Party("/my").Layout("layouts/mylayout.html") - // { - // my.Get("/", func(ctx context.Context) { - // ctx.MustRender("page1.html", nil) - // }) - // } + // my.Get("/", func(ctx iris.Context) { + // ctx.View("page1.html") + // }) + // + // Examples: https://github.com/kataras/iris/tree/master/_examples/view Layout(tmplLayoutFile string) Party } diff --git a/hero/func_result_test.go b/hero/func_result_test.go index 54a62c6270..6c602b6502 100644 --- a/hero/func_result_test.go +++ b/hero/func_result_test.go @@ -81,6 +81,22 @@ func GetCustomStructWithError(ctx iris.Context) (s testCustomStruct, err error) return } +type err struct { + Status int `json:"status_code"` + Message string `json:"message"` +} + +func (e err) Dispatch(ctx iris.Context) { + // write the status code based on the err's StatusCode. + ctx.StatusCode(e.Status) + // send to the client the whole object as json + ctx.JSON(e) +} + +func GetCustomErrorAsDispatcher() err { + return err{iris.StatusBadRequest, "this is my error as json"} +} + func TestFuncResult(t *testing.T) { app := iris.New() h := New() @@ -102,6 +118,7 @@ func TestFuncResult(t *testing.T) { app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk)) app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType)) app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError)) + app.Get("/custom/error/as/dispatcher", h.Handler(GetCustomErrorAsDispatcher)) e := httptest.New(t, app) @@ -149,4 +166,10 @@ func TestFuncResult(t *testing.T) { // the content should be not JSON it should be the status code's text // it will fire the error's text Body().Equal("omit return of testCustomStruct and fire error") + + e.GET("/custom/error/as/dispatcher").Expect(). + Status(iris.StatusBadRequest). // the default status code if error is not nil + // the content should be not JSON it should be the status code's text + // it will fire the error's text + JSON().Equal(err{iris.StatusBadRequest, "this is my error as json"}) } diff --git a/httptest/httptest.go b/httptest/httptest.go index 91b8525884..59c59cc2f9 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -87,7 +87,12 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe // set the logger or disable it (default) and disable the updater (for any case). app.Configure(iris.WithoutVersionChecker) app.Logger().SetLevel(conf.LogLevel) - app.Build() + if err := app.Build(); err != nil { + if conf.Debug && (conf.LogLevel == "disable" || conf.LogLevel == "disabled") { + app.Logger().Println(err.Error()) + return nil + } + } testConfiguration := httpexpect.Config{ BaseURL: conf.URL,