DEV Community

Gerasimos (Makis) Maropoulos
Gerasimos (Makis) Maropoulos

Posted on • Edited on

Iris version 11.2 released

It is my pleasure and honor to spread the news about the new Iris release.

As open-source project authors and/or managers, we owe a lot to our users - to our co end-developers that they learn and work with our project, no matter how big or small it is, spreading its potentials to their co-workers and learning the deep and nice parts of a whole programming language through our guideliness.

We are all exciting when a feature request of us goes live to a popular project, right? This happens almost every week, to Iris, every week a new user feature request is discussed, accepted and finally implemented. Iris is not just another open source web framework written in Go. It is a Community.

This release is not an exception to that long-term tradition. Iris version 11.2 is done through 130 new commits to the main repository and 151 commits to its new websocket implementation, neffos repository:

  • 17 bugfixes and minor improvements
    • 13 of 17 bugfixes and improvements are reported and requested by its end-users themselves!
  • 5 new features and major improvements
  • all examples and middlewares are updated and tested with go 1.12

Let's start with the most easy to use feature for your daily development with Iris.

Automatic Public Address with TLS

Wouldn't be great to test your web application server in a more "real-world environment" like a public, remote, address instead of localhost?

There are plenty of third-party tools offering such a feature, but in my opinion, the ngrok one is the best among them. It's popular and tested for years, like Iris, in fact, it has ~600 stars more than Iris itself. Great job @inconshreveable!

Iris v11.2 offers ngrok integration. This feature is simple yet very powerful. It really helps when you want to quickly show your development progress to your colleagues or the project leader at a remote conference.

Follow the steps below to, temporarily, convert your local Iris web server to a public one.

  1. Go head and download ngrok, add it to your $PATH environment variable,
  2. Simply pass the WithTunneling configurator in your app.Run,
  3. You are ready to GO!

tunneling_screenshot

  • ctx.Application().ConfigurationReadOnly().GetVHost() returns the public domain value. Rarely useful but it's there for you. Most of the times you use relative url paths instead of absolute(or you should to).
  • It doesn't matter if ngrok is already running or not, Iris framework is smart enough to use ngrok's web API to create a tunnel.

Full Tunneling configuration:

app.Run(iris.Addr(":8080"), iris.WithConfiguration(
    iris.Configuration{
        Tunneling: iris.TunnelingConfiguration{
            AuthToken:    "my-ngrok-auth-client-token",
            Bin:          "/bin/path/for/ngrok",
            Region:       "eu",
            WebInterface: "127.0.0.1:4040",
            Tunnels: []iris.Tunnel{
                {
                    Name: "MyApp",
                    Addr: ":8080",
                },
            },
        },
}))
Enter fullscreen mode Exit fullscreen mode

Routing: Handle different parameter types on the same path

Something like this works now without any issues (order: top as fallback)

app.Get("/u/{username:string}", func(ctx iris.Context) {
    ctx.Writef("before username (string), current route name: %s\n", ctx.RouteName())
    ctx.Next()
}, func(ctx iris.Context) {
    ctx.Writef("username (string): %s", ctx.Params().Get("username"))
})

app.Get("/u/{id:int}", func(ctx iris.Context) {
    ctx.Writef("before id (int), current route name: %s\n", ctx.RouteName())
    ctx.Next()
}, func(ctx iris.Context) {
    ctx.Writef("id (int): %d", ctx.Params().GetIntDefault("id", 0))
})

app.Get("/u/{uid:uint}", func(ctx iris.Context) {
    ctx.Writef("before uid (uint), current route name: %s\n", ctx.RouteName())
    ctx.Next()
}, func(ctx iris.Context) {
    ctx.Writef("uid (uint): %d", ctx.Params().GetUintDefault("uid", 0))
})

app.Get("/u/{firstname:alphabetical}", func(ctx iris.Context) {
    ctx.Writef("before firstname (alphabetical), current route name: %s\n", ctx.RouteName())
    ctx.Next()
}, func(ctx iris.Context) {
    ctx.Writef("firstname (alphabetical): %s", ctx.Params().Get("firstname"))
})

/*
    /u/abcd maps to :alphabetical (if :alphabetical registered otherwise :string)
    /u/42 maps to :uint (if :uint registered otherwise :int)
    /u/-1 maps to :int (if :int registered otherwise :string)
    /u/abcd123 maps to :string
*/
Enter fullscreen mode Exit fullscreen mode

Content Negotiation

Sometimes a server application needs to serve different representations of a resource at the same URI. Of course this can be done by hand, manually checking the Accept request header and push the requested form of the content. However, as your app manages more resources and different kind of representations this can be very painful, as you may need to check for Accept-Charset, Accept-Encoding, put some server-side priorities, handle the errors correctly and e.t.c.

There are some web frameworks in Go already struggle to implement a feature like this but they don't do it correctly:

  • they don't handle accept-charset at all
  • they don't handle accept-encoding at all
  • they don't send error status code (406 not acceptable) as RFC proposes and more...

But, fortunately for us, Iris always follows the best practises and the Web standards.

Based on:

type testdata struct {
    Name string `json:"name" xml:"Name"`
    Age  int    `json:"age" xml:"Age"`
}
Enter fullscreen mode Exit fullscreen mode

Render a resource with "gzip" encoding algorithm
as application/json or text/xml or application/xml

  • when client's accept header contains one of them
  • or JSON (the first declared) if accept is empty,
  • and when client's accept-encoding header contains "gzip" or it's empty.
app.Get("/resource", func(ctx iris.Context) {
    data := testdata{
        Name: "test name",
        Age:  26,
    }

        ctx.Negotiation().JSON().XML().EncodingGzip()

    _, err := ctx.Negotiate(data)
    if err != nil {
        ctx.Writef("%v", err)
    }
})

Enter fullscreen mode Exit fullscreen mode

OR define them in a middleware and call Negotiate with nil in the final handler.

ctx.Negotiation().JSON(data).XML(data).Any("content for */*")
ctx.Negotiate(nil)
Enter fullscreen mode Exit fullscreen mode
app.Get("/resource2", func(ctx iris.Context) {
    jsonAndXML := testdata{
        Name: "test name",
        Age:  26,
    }

    ctx.Negotiation().
        JSON(jsonAndXML).
        XML(jsonAndXML).
        HTML("<h1>Test Name</h1><h2>Age 26</h2>")

    ctx.Negotiate(nil)
})
Enter fullscreen mode Exit fullscreen mode

Read the full example.

The Context.Negotiation method creates once and returns the negotiation builder
to build server-side available prioritized content for specific content type(s), charset(s) and encoding algorithm(s).

Context.Negotiation() *context.NegotiationBuilder
Enter fullscreen mode Exit fullscreen mode

The Context.Negotiate method used for serving different representations of a resource at the same URI. It returns context.ErrContentNotSupported when not matched mime type(s).

Context.Negotiate(v interface{}) (int, error)
Enter fullscreen mode Exit fullscreen mode

The new Websocket package

There are times that you simply can't improve something without a breaking change. After a year and a half without breaking changes, this version of Iris introduces two breaking changes for the best. The first one is the websocket module which was fully re-written and the second has to do with how you serve system (or embedded) directories.

The new websocket package, which is selfhosted at https://github.com/kataras/neffos, is a work of 4 months daily designing, coding, re-designing and refactoring.

Even there, from day-zero, users immediately started to be participated by asking questions and making proposals. Of course, as our trandition, they are discussed (a lot) and are all available by now:

Broadcast message to a Connection ID

IDGenerator for Iris

Server Ask method like Conn.Ask

Add a cron example

Adapters support for scalability

The new websocket implementation is far better and faster at all use cases than we had previously and without the bugs and the compromises we had to deal brecause of the no-breaking-changes rule of the previous versions. Unlike the previous one which had only a simple go client that new one provides clients for Go and Typescript/Javascript(both nodejs and browser-side) and anyone can make a client for any language, C++ for example with ease. I can say that our new websocket module is very unique but feels like home with a lot of preparation and prototyping under the hoods. The result worth the days and nights I spent on this thing -- of course, you - as community will prove that point, based on your feedback in the end of the day.

Let's see what the new version of websocket package offers that the previous v11.1.x one couldn't handle.

Feature v11.1.x v11.2.x (neffos)
Scale-out using Nats or Redis NO YES
Gorilla Protocol Implementation YES YES
Gobwas/ws Protocol Implementation NO YES
Acknowledgements YES YES
Namespaces NO YES
Rooms YES YES
Broadcast YES(but slow) YES(faster than socket.io and everything else we've tested)
Event-Driven architecture YES YES
Request-Response architecture NO YES
Error Awareness NO YES
Asynchronous Broadcast NO YES
Timeouts YES YES
Encoding YES (only JSON) YES
Native WebSocket Messages YES YES
Reconnection NO YES
Modern client for Browsers, Nodejs and Go NO YES

Except the new selfhosted neffos repository. The kataras/iris/websocket subpackage now contains (only) Iris-specific migrations and helpers for the neffos websocket framework one.

For example, to gain access of the request's Context you can call the websocket.GetContext(Conn) from inside an event message handler/callback:

// GetContext returns the Iris Context from a websocket connection.
func GetContext(c *neffos.Conn) Context
Enter fullscreen mode Exit fullscreen mode

To register a websocket neffos.Server to a route use the websocket.Handler function:

// IDGenerator is an iris-specific IDGenerator for new connections.
type IDGenerator func(Context) string

// Handler returns an Iris handler to be served in a route of an Iris application.
// Accepts the neffos websocket server as its first input argument
// and optionally an Iris-specific `IDGenerator` as its second one.
func Handler(s *neffos.Server, IDGenerator ...IDGenerator) Handler
Enter fullscreen mode Exit fullscreen mode

Usage

import (
    "github.com/kataras/neffos"
    "github.com/kataras/iris/websocket"
)

// [...]

onChat := func(ns *neffos.NSConn, msg neffos.Message) error {
    ctx := websocket.GetContext(ns.Conn)
    // [...]
    return nil
}

app := iris.New()
ws := neffos.New(websocket.DefaultGorillaUpgrader, neffos.Namespaces{
    "default": neffos.Events {
        "chat": onChat,
    },
})
app.Get("/websocket_endpoint", websocket.Handler(ws))
Enter fullscreen mode Exit fullscreen mode

MVC | The new Websocket Controller

The neffos package contains a feature to create events from Go struct values, its NewStruct package-level function. In addition, Iris has its own iris/mvc/Application.HandleWebsocket(v interface{}) *neffos.Struct to register controllers in existing Iris MVC applications(offering a fully featured dependency injection container for request values and static services) like any regular HTTP Controllers you are used to.

// HandleWebsocket handles a websocket specific controller.
// Its exported methods are the events.
// If a "Namespace" field or method exists then namespace is set,
// otherwise empty namespace will be used for this controller.
//
// Note that a websocket controller is registered and ran under
// a connection connected to a namespace
// and it cannot send HTTP responses on that state.
// However all static and dynamic dependencies behave as expected.
func (*mvc.Application) HandleWebsocket(controller interface{}) *neffos.Struct
Enter fullscreen mode Exit fullscreen mode

Let's see a usage example, we want to bind the OnNamespaceConnected, OnNamespaceDisconnect built-in events and a custom "OnChat" event with our controller's methods.

1. We create the controller by declaring a NSConn type field as stateless and write the methods we need.

type websocketController struct {
    *neffos.NSConn `stateless:"true"`
    Namespace string

    Logger MyLoggerInterface
}

func (c *websocketController) OnNamespaceConnected(msg neffos.Message) error {
    return nil
}

func (c *websocketController) OnNamespaceDisconnect(msg neffos.Message) error {
    return nil
}

func (c *websocketController) OnChat(msg neffos.Message) error {
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Iris is smart enough to catch the Namespace string struct field to use it to register the controller's methods as events for that namespace, alternatively you can create a controller method of Namespace() string { return "default" } or use the HandleWebsocket's return value to .SetNamespace("default"), it's up to you.

2. We inititalize our MVC application targets to a websocket endpoint, as we used to do with regular HTTP Controllers for HTTP routes.

import (
    // [...]
    "github.com/kataras/iris/mvc"
)
// [app := iris.New...]

mvcApp := mvc.New(app.Party("/websocket_endpoint"))
Enter fullscreen mode Exit fullscreen mode

3. We register our dependencies, if any.

mvcApp.Register(
    &prefixedLogger{prefix: "DEV"},
)
Enter fullscreen mode Exit fullscreen mode

4. We register one or more websocket controllers, each websocket controller maps to one namespace (just one is enough, as in most of the cases you don't need more, but that depends on your app's needs and requirements).

mvcApp.HandleWebsocket(&websocketController{Namespace: "default"})
Enter fullscreen mode Exit fullscreen mode

5. Next, we continue by mapping the mvc application as a connection handler to a websocket server (you may use more than one mvc applications per websocket server via neffos.JoinConnHandlers(mvcApp1, mvcApp2)).

websocketServer := neffos.New(websocket.DefaultGorillaUpgrader, mvcApp)
Enter fullscreen mode Exit fullscreen mode

6. And the last step is to register that server to our endpoint through a normal .Get method.

mvcApp.Router.Get("/", websocket.Handler(websocketServer))
Enter fullscreen mode Exit fullscreen mode

We will not cover the whole neffos package here, there are a lot of new features. Don't be afraid, you can still do all the things you did previously without a lot of learning process but as you going further to more advanced applications you can achieve more by reading its wiki page. In fact there are so many new things that are written in a e-book which you can request direct online access 100% free.

Examples

Interesting? Continue the reading by navigating to the learning neffos section.

The new FileServer

We will continue by looking the new FileServer package-level function and Party.HandleDir method.

Below is a list of the functions and methods we were using so far(as of v11.1.x):

  1. Party.StaticWeb(requestPath string, systemPath string) *Route * (the most commonly used)
  2. func NewStaticHandlerBuilder(dir string) StaticHandlerBuilder *
  3. func StaticHandler(systemPath string, showList bool, gzip bool) Handler *
  4. Party.StaticHandler(systemPath string, showList bool, gzip bool) Handler *
  5. Party.StaticServe(systemPath string, requestPath ...string) *Route *
  6. func StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string, assetsGziped bool) Handler *
  7. Party.StaticEmbeddedGzip(requestPath string, vdir string, gzipAssetFn func(name string) ([]byte, error), gzipNamesFn func() []string) *Route *
  8. Party.StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route *
  9. Application.SPA(assetHandler Handler) *router.SPABuilder *

That is a hell of functions that doing slightly differnet things but all resulting to the same functionality that an Iris-Dev wants in the end. Also, the embedded file server was missing an important feature that a (physical) system's file server had, serve by content range (to be fair with ourselves, we weren't alone, the rest of third party tools and frameworks don't even have or think the half features that we provided to our users for embedded files, including this one).

So, I was wondering, in the spirit that we are free of the no-breaking-changes rule for this release on the websocket level, to bring some break changes outside of the websocket module too by not just replacing but also removing all existing static handler functions, however I came up to the decision that it's better to let them exist for one major version more ~and call the new methods under the hoods but~ with a deprecation warning that will be logged to the dev's terminal. Supposedly you had a main.go and on its line 18 app.StaticWeb("/static", "./assets") exists, the error will look like that:

deprecation_output_example

Note the hover, most code editors will navigate you to the source of the problem, the deprecation log takes the parameter values of the deprecated method, in that case the StaticWeb and suggests the new way.

All those functions can be replaced with a single one package-level and one Party method. The package-level function gives you an Handler to work with and the other Party method will register routes on subdomain, subrouter and etc. At this point I am writing the issue I already completed this feature locally, not yet pushed but will be soon. It looks like that:

FileServer(directory string, options ...DirOptions) Handler
Enter fullscreen mode Exit fullscreen mode
Party.HandleDir(requestPath string, directory string, options ...DirOptions) *Route
Enter fullscreen mode Exit fullscreen mode

Where the DirOptions are:

type DirOptions struct {
// Defaults to "/index.html", if request path is ending with **/*/$IndexName
// then it redirects to **/*(/) which another handler is handling it,
// that another handler, called index handler, is auto-registered by the framework
// if end developer wasn't managed to handle it manually/by hand.
IndexName string
// Should files served under gzip compression?
Gzip bool

// List the files inside the current requested directory if `IndexName` not found.
ShowList bool
// If `ShowList` is true then this function will be used instead
// of the default one to show the list of files of a current requested directory(dir).
DirList func(ctx Context, dirName string, dir http.File) error

// When embedded.
Asset      func(name string) ([]byte, error)   
AssetInfo  func(name string) (os.FileInfo, error)
AssetNames func() []string

// Optional validator that loops through each found requested resource.
AssetValidator func(ctx Context, name string) bool
}
Enter fullscreen mode Exit fullscreen mode

If you used one of the above methods, refactoring your project's static file serving code blocks is highly recommended, it's quite easy in fact, here is how you can do it:

Party.StaticWeb and Party.StaticServe

v11.1.x

app.StaticWeb("/static", "./assets")
Enter fullscreen mode Exit fullscreen mode

v11.2.x

app.HandleDir("/static", "./assets")
Enter fullscreen mode Exit fullscreen mode

If you used the StaticWeb/StaticServe, just make a replace-to-all-files to HandleDir operation in your code editor and you're done.

StaticHandler

v11.1.x

handler := iris.StaticHandler("./assets", true, true)
Enter fullscreen mode Exit fullscreen mode

v11.2.x

handler := iris.FileServer("./assets", iris.DirOptions {ShowList: true, Gzip: true})
Enter fullscreen mode Exit fullscreen mode

StaticEmbeddedHandler

v11.1.x

handler := iris.StaticEmbeddedHandler("./assets", Asset, AssetNames, true)
Enter fullscreen mode Exit fullscreen mode

v11.2.x

handler := iris.FileServer("./assets", iris.DirOptions {
  Asset: Asset,
  AssetInfo: AssetInfo,
  AssetNames: AssetNames,
  Gzip: true})
Enter fullscreen mode Exit fullscreen mode

Party.StaticEmbedded and Party.StaticEmbeddedGzip

v11.1.x

app.StaticEmbedded("/static", "./assets", Asset, AssetNames)
Enter fullscreen mode Exit fullscreen mode

v11.2.x

app.HandleDir("/static", "./assets", iris.DirOptions {
  Asset: Asset,
  AssetInfo: AssetInfo,
  AssetNames: AssetNames,
  Gzip: true/false})
Enter fullscreen mode Exit fullscreen mode

Application.SPA

v11.1.x

app.RegisterView(iris.HTML("./public", ".html"))

app.Get("/", func(ctx iris.Context) {
    ctx.ViewData("Page", page)
    ctx.View("index.html")
})

assetHandler := app.StaticHandler("./public", false, false)
app.SPA(assetHandler)
Enter fullscreen mode Exit fullscreen mode

v11.2.x

app.RegisterView(iris.HTML("./public", ".html"))

// Overrides the file server's index route. 
// Order of this route registration does not matter.
app.Get("/", func(ctx iris.Context) {
    ctx.ViewData("Page", page)
    ctx.View("index.html")
})

app.HandleDir("/", "./public")
Enter fullscreen mode Exit fullscreen mode

The above changes are not only syntactical. Unlike the standard net/http design we give the chance and the features to the end-developer to use different handlers for index files to customize the middlewares and any other options and code that required when designing a Single Page Applications.
Previously something like /static/index.html -> /static should be manually handled by developer through app.Get to serve a directory's index.html file. Now, if a handler like this is missing then the framework will register it automatically, order of route registration does not even matter, Iris handles them on build state. Another new feature is that now the file server can handle content-range embedded files and also show a list of files in an embedded directory via the DirOptions.ShowList exactly like the system directories.

The above FileServer function and HandleDir method handles every case in a single spot, all previous and new features are live inside those two.

As a result from the 9(nine) functions and methods we had, we end up with just 2(two) with less code, more improvements and new features. That fact gives any user, experienced or newcomer an ideal place to start working without searching and reading more than they need to.

New Jet View Engine

This version contains a new new View Engine for the jet template parser as requested at: https://github.com/kataras/iris/issues/1281


tmpl := iris.Jet("./views", ".jet")
app.RegisterView(tmpl)
Enter fullscreen mode Exit fullscreen mode

Bugfixes and minor improvements

Let's continue by listing the minor bugfixes, improvements and new functions. For more information check the links after the function or method declaration.

1. Context.FullRequestURI() - as requested at: https://github.com/kataras/iris/issues/1167.

2. NewConditionalHandler(filter func(ctx Context) bool, handlers ...Handler) Handler as requested at: https://github.com/kataras/iris/issues/1170.

3. Context.ResetRequest(newReq *http.Request) as requested at: https://github.com/kataras/iris/issues/1180.

4. Fix Context.StopExecution() wasn't respect by MVC controller's methods, as reported at: https://github.com/kataras/iris/issues/1187.

5. Give the ability to modify the whole session's cookie on Start and Update/ShiftExpiration methods and add a StartWithPath helper as requested at: https://github.com/kataras/iris/issues/1186.

6. Add Context.ResponseWriter().IsHijacked() bool to report whether the underline connection is hijacked or not.

7. Add the ability to intercept the default error handler by seting a custom ErrorHandler to MVC application-level or per controller as requested at: https://github.com/kataras/iris/issues/1244:


mvcApp := mvc.New(app)
mvcApp.HandleError(func(ctx iris.Context, err error) {
    ctx.HTML(fmt.Sprintf("<b>%s</b>", err.Error()))
})
// OR

type myController struct { /* [...] */ }

// Overriddes the mvcApp.HandleError function.
func (c *myController) HandleError(ctx iris.Context, err error) {
    ctx.HTML(fmt.Sprintf("<i>%s</i>", err.Error()))
}
Enter fullscreen mode Exit fullscreen mode

8. Extract the Delim configuration field for redis sessiondb as requested at: https://github.com/kataras/iris/issues/1256. And replace the underline redis client library to the radix one.

9. Fix hero/mvc when map, struct, slice return nil as null in JSON responses, as reported at: https://github.com/kataras/iris/issues/1273.

10. Enable view.Django pongo2 addons as requested at: https://github.com/kataras/iris/issues/1284.

11. Add mvc#Before/AfterActivation.HandleMany and GetRoutes methods as requested at: https://github.com/kataras/iris/issues/1292.

12. Fix WithoutBodyConsumptionOnUnmarshal option not be respected on Context.ReadForm and Context.FormValues as reported at: https://github.com/kataras/iris/issues/1297.

13. Fix jwt, casbin and go-i81n middlewares.

14. Fix https://github.com/kataras/iris/issues/1298.

15. Add ReadQuery as requested at: https://github.com/kataras/iris/issues/1207.

16. Easy way to register a session as a middleware. Add sessions/Sessions#Handler and package-level sessions.Get function (examples below).

Debugging

  1. Warning messages for invalid registration of MVC dependencies and controllers fields or method input arguments.
  2. Print information for MVC Controller's method maps to a websocket event.
  3. Context.RouteName() which returns the current route's name.
  4. Context.HandlerFileName() which returns the exact program's source code position of the current handler function that it's being executed (file:line).

Examples

Iris offers more than 110 examples for both experienced and new gophers.

New Examples

Updated Examples

Top comments (0)