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.
- Go head and download ngrok, add it to your $PATH environment variable,
- Simply pass the
WithTunneling
configurator in yourapp.Run
, - You are ready to GO!
-
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",
},
},
},
}))
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
*/
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:
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
type testdata struct {
Name string `json:"name" xml:"Name"`
Age int `json:"age" xml:"Age"`
}
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)
}
})
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)
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)
})
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
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).
- The "v" can be a single iris.N struct value.
- The "v" can be any value completes the context.ContentSelector interface.
- The "v" can be any value completes the context.ContentNegotiator interface.
- The "v" can be any value of struct(JSON, JSONP, XML, YAML) or string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type.
If the "v" is nil, the
Context.Negotitation()
builder's
content will be used instead, otherwise "v" overrides builder's content
(server mime types are still retrieved by its registered, supported, mime list)Set mime type priorities by Negotiation().MIME.Text.JSON.XML.HTML....
Set charset priorities by Negotiation().Charset(...).
Set encoding algorithm priorities by Negotiation().Encoding(...).
Modify the accepted by Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)....
Context.Negotiate(v interface{}) (int, error)
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
Server Ask method like Conn.Ask
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
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
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))
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
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
}
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"))
3. We register our dependencies, if any.
mvcApp.Register(
&prefixedLogger{prefix: "DEV"},
)
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"})
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)
6. And the last step is to register that server to our endpoint through a normal .Get
method.
mvcApp.Router.Get("/", websocket.Handler(websocketServer))
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):
-
Party.StaticWeb(requestPath string, systemPath string) *Route
* (the most commonly used) -
func NewStaticHandlerBuilder(dir string) StaticHandlerBuilder
* -
func StaticHandler(systemPath string, showList bool, gzip bool) Handler
* -
Party.StaticHandler(systemPath string, showList bool, gzip bool) Handler
* -
Party.StaticServe(systemPath string, requestPath ...string) *Route
* -
func StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string, assetsGziped bool) Handler
* -
Party.StaticEmbeddedGzip(requestPath string, vdir string, gzipAssetFn func(name string) ([]byte, error), gzipNamesFn func() []string) *Route
* -
Party.StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) *Route
* -
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:
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
Party.HandleDir(requestPath string, directory string, options ...DirOptions) *Route
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
}
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")
v11.2.x
app.HandleDir("/static", "./assets")
If you used the
StaticWeb/StaticServe
, just make a replace-to-all-files toHandleDir
operation in your code editor and you're done.
StaticHandler
v11.1.x
handler := iris.StaticHandler("./assets", true, true)
v11.2.x
handler := iris.FileServer("./assets", iris.DirOptions {ShowList: true, Gzip: true})
StaticEmbeddedHandler
v11.1.x
handler := iris.StaticEmbeddedHandler("./assets", Asset, AssetNames, true)
v11.2.x
handler := iris.FileServer("./assets", iris.DirOptions {
Asset: Asset,
AssetInfo: AssetInfo,
AssetNames: AssetNames,
Gzip: true})
Party.StaticEmbedded and Party.StaticEmbeddedGzip
v11.1.x
app.StaticEmbedded("/static", "./assets", Asset, AssetNames)
v11.2.x
app.HandleDir("/static", "./assets", iris.DirOptions {
Asset: Asset,
AssetInfo: AssetInfo,
AssetNames: AssetNames,
Gzip: true/false})
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)
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")
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)
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()))
}
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
- Warning messages for invalid registration of MVC dependencies and controllers fields or method input arguments.
- Print information for MVC Controller's method maps to a websocket event.
-
Context.RouteName()
which returns the current route's name. -
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
- Serve using HTTP/3 Quic as requested at: https://github.com/kataras/iris/issues/1295
- Public domain address
- Build RESTful API with the official MongoDB Go Driver and Iris
- Yet another dependency injection example and good practises at general
- MVC Regexp
- Jet View Engine and Embedded Jet templates
- Websocket
- GORM
- ReadQuery
- Sessions Middleware
- Content Negotiation
- Read YAML
Updated Examples
- Custom Router Wrapper
- FileServer Basics
- Embedding Files Into App Executable File
- Embedding Gziped Files Into App Executable File
- Single Page Application
- Embedded Single Page Application
- Embedded Single Page Application with other routes
- Websocket Native Messages
- Websocket Controller
- Using the Redis Session Database