add context#StatusCodeNotSuccessful for customize even the most customized clients that are not compatible with the standards and fix the SPA if static file serve handlers are passed as its AssetHandler as reported at the chat.iris-go.com

Former-commit-id: ccd0815a09b9305bfbeaad7b46559dd86f34f20b
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-01-31 02:35:22 +02:00
parent e38ea65dc7
commit 019911237c
12 changed files with 213 additions and 164 deletions

View File

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

View File

@ -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"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

167
core/router/spa.go Normal file
View File

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

View File

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