diff --git a/_examples/file-server/single-page-application/basic/main.go b/_examples/file-server/single-page-application/basic/main.go index 6ed45228..649f7066 100644 --- a/_examples/file-server/single-page-application/basic/main.go +++ b/_examples/file-server/single-page-application/basic/main.go @@ -21,7 +21,7 @@ func newApp() *iris.Application { }) // or just serve index.html as it is: - // app.Get("/", func(ctx iris.Context) { + // app.Get("/{f:path}", func(ctx iris.Context) { // ctx.ServeFile("index.html", false) // }) diff --git a/_examples/routing/README.md b/_examples/routing/README.md index f57c4849..5b85c202 100644 --- a/_examples/routing/README.md +++ b/_examples/routing/README.md @@ -611,7 +611,8 @@ func main(){ app := iris.New() app.OnErrorCode(iris.StatusNotFound, notFound) app.OnErrorCode(iris.StatusInternalServerError, internalServerError) - // to register a handler for all status codes >=400: + // to register a handler for all "error" status codes(context.StatusCodeNotSuccessful) + // defaults to < 200 || >= 400: // app.OnAnyErrorCode(handler) app.Get("/", index) app.Run(iris.Addr(":8080")) diff --git a/_examples/structuring/bootstrap/bootstrap/bootstrapper.go b/_examples/structuring/bootstrap/bootstrap/bootstrapper.go index 46f26fbf..9efa7df6 100644 --- a/_examples/structuring/bootstrap/bootstrap/bootstrapper.go +++ b/_examples/structuring/bootstrap/bootstrap/bootstrapper.go @@ -64,7 +64,8 @@ func (b *Bootstrapper) SetupWebsockets(endpoint string, onConnection websocket.C }) } -// SetupErrorHandlers prepares the http error handlers (>=400). +// SetupErrorHandlers prepares the http error handlers +// `(context.StatusCodeNotSuccessful`, which defaults to < 200 || >= 400 but you can change it). func (b *Bootstrapper) SetupErrorHandlers() { b.OnAnyErrorCode(func(ctx iris.Context) { err := iris.Map{ diff --git a/configuration.go b/configuration.go index 2d8e8cde..2ab09e3b 100644 --- a/configuration.go +++ b/configuration.go @@ -460,13 +460,14 @@ type Configuration struct { DisableBodyConsumptionOnUnmarshal bool `json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"` // DisableAutoFireStatusCode if true then it turns off the http error status code handler automatic execution - // from "context.StatusCode(>=400)" and instead app should manually call the "context.FireStatusCode(>=400)". + // from (`context.StatusCodeNotSuccessful`, defaults to < 200 || >= 400). + // If that is false then for a direct error firing, then call the "context#FireStatusCode(statusCode)" manually. // // By-default a custom http error handler will be fired when "context.StatusCode(code)" called, - // code should be >=400 in order to be received as an "http error handler". + // code should be equal with the result of the the `context.StatusCodeNotSuccessful` in order to be received as an "http error handler". // // Developer may want this option to setted as true in order to manually call the - // error handlers when needed via "context.FireStatusCode(>=400)". + // error handlers when needed via "context#FireStatusCode(< 200 || >= 400)". // HTTP Custom error handlers are being registered via app.OnErrorCode(code, handler)". // // Defaults to false. diff --git a/context/context.go b/context/context.go index 35a91896..05ea0174 100644 --- a/context/context.go +++ b/context/context.go @@ -1042,6 +1042,21 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.writer.BeginResponse(w) } +// StatusCodeNotSuccessful defines if a specific "statusCode" is not +// a valid status code for a successful response. +// It defaults to < 200 || >= 400 +// +// Read more at `iris#DisableAutoFireStatusCode`, `iris/core/router#ErrorCodeHandler` +// and `iris/core/router#OnAnyErrorCode` for relative information. +// +// Do NOT change it. +// +// It's exported for extreme situations--special needs only, when the Iris server and the client +// is not following the RFC: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +var StatusCodeNotSuccessful = func(statusCode int) bool { + return statusCode < 200 || statusCode >= 400 +} + // EndRequest is executing once after a response to the request was sent and this context is useless or released. // // To follow the iris' flow, developer should: @@ -1049,7 +1064,7 @@ func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { // 2. release the response writer // and any other optional steps, depends on dev's application type. func (ctx *context) EndRequest() { - if ctx.GetStatusCode() >= 400 && + if StatusCodeNotSuccessful(ctx.GetStatusCode()) && !ctx.Application().ConfigurationReadOnly().GetDisableAutoFireStatusCode() { // author's note: // if recording, the error handler can handle diff --git a/context/response_recorder.go b/context/response_recorder.go index c469dc25..0157a94f 100644 --- a/context/response_recorder.go +++ b/context/response_recorder.go @@ -189,7 +189,7 @@ func (w *ResponseRecorder) WriteTo(res ResponseWriter) { if to, ok := res.(*ResponseRecorder); ok { - // set the status code, to is first ( probably an error >=400) + // set the status code, to is first ( probably an error? (context.StatusCodeNotSuccessful, defaults to < 200 || >= 400). if statusCode := w.ResponseWriter.StatusCode(); statusCode == defaultStatusCode { to.WriteHeader(statusCode) } diff --git a/context/transaction.go b/context/transaction.go index bd0442b1..e0f580ac 100644 --- a/context/transaction.go +++ b/context/transaction.go @@ -20,7 +20,7 @@ func (err TransactionErrResult) Error() string { // IsFailure returns true if this is an actual error func (err TransactionErrResult) IsFailure() bool { - return err.StatusCode >= 400 + return StatusCodeNotSuccessful(err.StatusCode) } // NewTransactionErrResult returns a new transaction result with the given error message, diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 5864aa1e..eed18a56 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -686,16 +686,6 @@ 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) - // } - // } - // } } requestPath = joinPath(requestPath, WildcardParam(paramName)) @@ -703,7 +693,7 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { } // OnErrorCode registers an error http status code -// based on the "statusCode" >= 400. +// based on the "statusCode" < 200 || >= 400 (came from `context.StatusCodeNotSuccessful`). // The handler is being wrapepd by a generic // handler which will try to reset // the body if recorder was enabled @@ -718,55 +708,16 @@ func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) } // OnAnyErrorCode registers a handler which called when error status code written. -// Same as `OnErrorCode` but registers all http error codes. -// See: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +// Same as `OnErrorCode` but registers all http error codes based on the `context.StatusCodeNotSuccessful` +// which defaults to < 200 || >= 400 for an error code, any previos error code will be overriden, +// so call it first if you want to use any custom handler for a specific error status code. +// +// Read more at: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) { - // we could register all >=400 and <=511 but this way - // could override custom status codes that iris developers can register for their - // web apps whenever needed. - // There fore these are the hard coded http error statuses: - var errStatusCodes = []int{ - http.StatusBadRequest, - http.StatusUnauthorized, - http.StatusPaymentRequired, - http.StatusForbidden, - http.StatusNotFound, - http.StatusMethodNotAllowed, - http.StatusNotAcceptable, - http.StatusProxyAuthRequired, - http.StatusRequestTimeout, - http.StatusConflict, - http.StatusGone, - http.StatusLengthRequired, - http.StatusPreconditionFailed, - http.StatusRequestEntityTooLarge, - http.StatusRequestURITooLong, - http.StatusUnsupportedMediaType, - http.StatusRequestedRangeNotSatisfiable, - http.StatusExpectationFailed, - http.StatusTeapot, - http.StatusUnprocessableEntity, - http.StatusLocked, - http.StatusFailedDependency, - http.StatusUpgradeRequired, - http.StatusPreconditionRequired, - http.StatusTooManyRequests, - http.StatusRequestHeaderFieldsTooLarge, - http.StatusUnavailableForLegalReasons, - http.StatusInternalServerError, - http.StatusNotImplemented, - http.StatusBadGateway, - http.StatusServiceUnavailable, - http.StatusGatewayTimeout, - http.StatusHTTPVersionNotSupported, - http.StatusVariantAlsoNegotiates, - http.StatusInsufficientStorage, - http.StatusLoopDetected, - http.StatusNotExtended, - http.StatusNetworkAuthenticationRequired} - - for _, statusCode := range errStatusCodes { - api.OnErrorCode(statusCode, handlers...) + for code := 100; code <= 511; code++ { + if context.StatusCodeNotSuccessful(code) { + api.OnErrorCode(code, handlers...) + } } } diff --git a/core/router/fs.go b/core/router/fs.go index ace1a494..d4745de4 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -254,7 +254,7 @@ func (w *fsHandler) Build() context.Handler { gzipEnabled) // check for any http errors after the file handler executed - if prevStatusCode >= 400 { // error found (404 or 400 or 500 usually) + if context.StatusCodeNotSuccessful(prevStatusCode) { // error found (404 or 400 or 500 usually) if writer, ok := ctx.ResponseWriter().(*context.GzipResponseWriter); ok && writer != nil { writer.ResetBody() writer.Disable() diff --git a/core/router/router_spa_wrapper.go b/core/router/router_spa_wrapper.go deleted file mode 100644 index 16b43e89..00000000 --- a/core/router/router_spa_wrapper.go +++ /dev/null @@ -1,91 +0,0 @@ -package router - -import ( - "strings" - - "github.com/kataras/iris/context" -) - -// AssetValidator returns true if "filename" -// is asset, i.e: strings.Contains(filename, "."). -type AssetValidator func(filename string) bool - -// SPABuilder helps building a single page application server -// which serves both routes and files from the root path. -type SPABuilder struct { - IndexNames []string - AssetHandler context.Handler - AssetValidators []AssetValidator -} - -// AddIndexName will add an index name. -// If path == $filename then it redirects to "/". -// -// It can be called after the `BuildWrapper ` as well but BEFORE the server start. -func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { - s.IndexNames = append(s.IndexNames, filename) - return s -} - -// NewSPABuilder returns a new Single Page Application builder -// It does what StaticWeb or StaticEmbedded expected to do when serving files and routes at the same time -// from the root "/" path. -// -// Accepts a static asset handler, which can be an app.StaticHandler, app.StaticEmbeddedHandler... -func NewSPABuilder(assetHandler context.Handler) *SPABuilder { - if assetHandler == nil { - assetHandler = func(ctx context.Context) { - ctx.Writef("empty asset handler") - } - } - - return &SPABuilder{ - IndexNames: nil, - // IndexNames is empty by-default, - // if the user wants to redirect to "/" from "/index.html" she/he can chage that to []string{"index.html"} manually. - AssetHandler: assetHandler, - AssetValidators: []AssetValidator{ - func(path string) bool { - return true // returns true by-default, if false then it fires 404. - }, - }, - } -} - -func (s *SPABuilder) isAsset(reqPath string) bool { - for _, v := range s.AssetValidators { - if !v(reqPath) { - return false - } - } - return true -} - -// Handler serves the asset handler but in addition, it makes some checks before that, -// based on the `AssetValidators` and `IndexNames`. -func (s *SPABuilder) Handler(ctx context.Context) { - path := ctx.Path() - - // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything - // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. - // - // It's here for backwards compatibility as well, see #803. - if !s.isAsset(path) { - // it's not asset, execute the registered route's handlers - ctx.NotFound() - return - } - - for _, index := range s.IndexNames { - if strings.HasSuffix(path, index) { - localRedirect(ctx, "./") - // "/" should be manually registered. - // We don't setup an index handler here, - // let full control to the user - // (use middleware, ctx.ServeFile or ctx.View and so on...) - return - } - } - - s.AssetHandler(ctx) -} diff --git a/core/router/spa.go b/core/router/spa.go new file mode 100644 index 00000000..0ff8ad5c --- /dev/null +++ b/core/router/spa.go @@ -0,0 +1,167 @@ +package router + +import ( + "strings" + + "github.com/kataras/iris/context" +) + +// AssetValidator returns true if "filename" +// is asset, i.e: strings.Contains(filename, "."). +type AssetValidator func(filename string) bool + +// SPABuilder helps building a single page application server +// which serves both routes and files from the root path. +type SPABuilder struct { + // Root defaults to "/", it's the root path that explicitly set-ed, + // this can be changed if more than SPAs are used on the same + // iris router instance. + Root string + // emptyRoot can be changed with `ChangeRoot` only, + // is, statically, true if root is empty + // and if root is empty then let 404 fire from server-side anyways if + // the passed `AssetHandler` returns 404 for a specific request path. + // Defaults to false. + emptyRoot bool + + IndexNames []string + AssetHandler context.Handler + AssetValidators []AssetValidator +} + +// AddIndexName will add an index name. +// If path == $filename then it redirects to Root, which defaults to "/". +// +// It can be called BEFORE the server start. +func (s *SPABuilder) AddIndexName(filename string) *SPABuilder { + s.IndexNames = append(s.IndexNames, filename) + return s +} + +// ChangeRoot modifies the `Root` request path that is +// explicitly set-ed if the `AssetHandler` gave a Not Found (404) +// previously, if request's path is the passed "path" +// then it explicitly sets that and it retries executing the `AssetHandler`. +// +// Empty Root means that let 404 fire from server-side anyways. +// +// Change it ONLY if you use more than one typical SPAs on the same Iris Application instance. +func (s *SPABuilder) ChangeRoot(path string) *SPABuilder { + s.Root = path + s.emptyRoot = path == "" + return s +} + +// NewSPABuilder returns a new Single Page Application builder +// It does what StaticWeb or StaticEmbedded expected to do when serving files and routes at the same time +// from the root "/" path. +// +// Accepts a static asset handler, which can be an app.StaticHandler, app.StaticEmbeddedHandler... +func NewSPABuilder(assetHandler context.Handler) *SPABuilder { + if assetHandler == nil { + assetHandler = func(ctx context.Context) { + ctx.Writef("empty asset handler") + } + } + + return &SPABuilder{ + Root: "/", + IndexNames: nil, + // "IndexNames" are empty by-default, + // if the user wants to redirect to "/" from "/index.html" she/he can chage that to []string{"index.html"} manually + // or use the `StaticHandler` as "AssetHandler" which does that already. + AssetHandler: assetHandler, + AssetValidators: []AssetValidator{ + func(path string) bool { + return true // returns true by-default, if false then it fires 404. + }, + }, + } +} + +func (s *SPABuilder) isAsset(reqPath string) bool { + for _, v := range s.AssetValidators { + if !v(reqPath) { + return false + } + } + return true +} + +// Handler serves the asset handler but in addition, it makes some checks before that, +// based on the `AssetValidators` and `IndexNames`. +func (s *SPABuilder) Handler(ctx context.Context) { + path := ctx.Path() + + // make a validator call, by-default all paths are valid and this codeblock doesn't mean anything + // but for cases that users wants to bypass an asset she/he can do that by modifiying the `APIBuilder#AssetValidators` field. + // + // It's here for backwards compatibility as well, see #803. + if !s.isAsset(path) { + // it's not asset, execute the registered route's handlers + ctx.NotFound() + return + } + + for _, index := range s.IndexNames { + if strings.HasSuffix(path, index) { + if s.emptyRoot { + ctx.NotFound() + return + } + localRedirect(ctx, "."+s.Root) + // s.Root should be manually registered to a route + // (not always, only if custom handler used). + // We don't setup an index handler here, + // let full control to the developer via "AssetHandler" + // (use of middleware, manually call of the ctx.ServeFile or ctx.View etc.) + return + } + } + + s.AssetHandler(ctx) + + if context.StatusCodeNotSuccessful(ctx.GetStatusCode()) && !s.emptyRoot && path != s.Root { + // If file was not something like a javascript file, or a css or anything that + // the passed `AssetHandler` scan-ed then re-execute the `AssetHandler` + // using the `Root` as the request path (virtually). + // + // If emptyRoot is true then + // fire the response as it's, "AssetHandler" is fully responsible for it, + // client-side's router for invalid paths will not work here else read below. + // + // Author's notes: + // the server doesn't need to know all client routes, + // client-side router is responsible for any kind of invalid paths, + // so explicit set to root path. + // + // The most simple solution was to use a + // func(ctx iris.Context) { ctx.ServeFile("$PATH/index.html") } as the "AssetHandler" + // but many developers use the `StaticHandler` (as shown in the examples) + // but it was not working as expected because it (correctly) fires + // a 404 not found if a file based on the request path didn't found. + // + // We can't just do it before the "AssetHandler"'s execution + // for two main reasons: + // 1. if it's a file serve handler, like `StaticHandler` then it will never serve + // the corresponding files! + // 2. it may manually handle those things, + // don't forget that "AssetHandler" can be + // ANY iris handler, so we can't be sure what the developer may want to do there. + // + // "AssetHandler" as the "StaticHandler" a retry doesn't hurt, + // it will give us a 404 if the file didn't found very fast WITHOUT moving to the + // rest of its validation and serving implementation. + // + // Another idea would be to modify the "AssetHandler" on every `ChangeRoot` + // call, which may give us some performance (ns) benefits + // but this could be bad if root is set-ed before the "AssetHandler", + // so keep it as it's. + rootURL, err := ctx.Request().URL.Parse(s.Root) + if err == nil { + ctx.Request().URL = rootURL + s.AssetHandler(ctx) + } + + } +} diff --git a/core/router/status.go b/core/router/status.go index 2099a78d..fc41fe81 100644 --- a/core/router/status.go +++ b/core/router/status.go @@ -7,6 +7,10 @@ import ( "github.com/kataras/iris/context" ) +func statusCodeSuccessful(statusCode int) bool { + return !context.StatusCodeNotSuccessful(statusCode) +} + // ErrorCodeHandler is the entry // of the list of all http error code handlers. type ErrorCodeHandler struct { @@ -21,7 +25,7 @@ type ErrorCodeHandler struct { func (ch *ErrorCodeHandler) Fire(ctx context.Context) { // if we can reset the body if w, ok := ctx.IsRecording(); ok { - if w.StatusCode() < 400 { // if not an error status code + if statusCodeSuccessful(w.StatusCode()) { // if not an error status code w.WriteHeader(ch.StatusCode) // then set it manually here, otherwise it should be setted via ctx.StatusCode(...) } // reset if previous content and it's recorder, keep the status code. @@ -109,14 +113,14 @@ func (s *ErrorCodeHandlers) Get(statusCode int) *ErrorCodeHandler { } // Register registers an error http status code -// based on the "statusCode" >= 400. +// based on the "statusCode" < 200 || >= 400 (`context.StatusCodeNotSuccessful`). // The handler is being wrapepd by a generic // handler which will try to reset // the body if recorder was enabled // and/or disable the gzip if gzip response recorder // was active. func (s *ErrorCodeHandlers) Register(statusCode int, handlers ...context.Handler) *ErrorCodeHandler { - if statusCode < 400 { + if statusCodeSuccessful(statusCode) { return nil } @@ -145,7 +149,7 @@ func (s *ErrorCodeHandlers) Register(statusCode int, handlers ...context.Handler // then it creates & registers a new trivial handler on the-fly. func (s *ErrorCodeHandlers) Fire(ctx context.Context) { statusCode := ctx.GetStatusCode() - if statusCode < 400 { + if statusCodeSuccessful(statusCode) { return } ch := s.Get(statusCode)