Core services

To get you off the ground, Flamego provides some core services that are essential to almost all web applications. However, you are not required to use all of them. The design principle of Flamego is always building the minimal core and pluggable addons at your own choice.

Context

Every handler invocation comes with its own request context, which is represented as the type flamego.ContextCore services - 图1open in new window. Data and state are not shared among these request contexts, which makes handler invocations independent from each other unless your web application has defined some other shared resources (e.g. database connections, cache).

Thereforce, flamego.Context is available to use out-of-the-box by your handlers:

  1. func main() {
  2. f := flamego.New()
  3. f.Get("/", func(c flamego.Context) string {
  4. ...
  5. })
  6. f.Run()
  7. }

Next

When a route is matched by a request, the Flame instance queues a chain of handlersCore services - 图2open in new window (including middleware) to be invoked in the same order as they are registered.

By default, the next handler will only be invoked after the previous one in the chain has finished. You may change this behvaior using the Next method, which allows you to pause the execution of the current handler and resume after the rest of the chain finished.

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/",
  9. func(c flamego.Context) {
  10. fmt.Println("starting the first handler")
  11. c.Next()
  12. fmt.Println("exiting the first handler")
  13. },
  14. func() {
  15. fmt.Println("executing the second handler")
  16. },
  17. )
  18. f.Run()
  19. }

When you run the above program and do curl http://localhost:2830/, the following logs are printed to your terminal:

  1. [Flamego] Listening on 0.0.0.0:2830 (development)
  2. starting the first handler
  3. executing the second handler
  4. exiting the first handler

The routing logger is taking advantage of this feature to collect the duration and status code of requestsCore services - 图3open in new window.

Remote address

Web applications often want to know where clients are coming from, then the RemoteAddr() method is the convenient helper made for you:

  1. func main() {
  2. f := flamego.New()
  3. f.Get("/", func(c flamego.Context) string {
  4. return "The remote address is " + c.RemoteAddr()
  5. })
  6. f.Run()
  7. }

The RemoteAddr() method is smarter than the standard library that only looks at the http.Request.RemoteAddr field (which stops working if your web application is behind a reverse proxy), it also takes into consideration of some well-known headers.

This method looks at following things in the order to determine which one is more likely to contain the real client address:

  • The X-Real-IP request header
  • The X-Forwarded-For request header
  • The http.Request.RemoteAddr field

This way, you can configure your reverse proxy to pass on one of these headers.

WARNING

The client can always fake its address using a proxy or VPN, getting the remote address is always considered as a best effort in web applications.

Redirect

The Redirect method is a shorthand for the http.RedirectCore services - 图4open in new window given the fact that the request context knows what the http.ResponseWriter and *http.Request are for the current request, and uses the http.StatusFound as the default status code for the redirection:

  • Code
  • Test
  1. package main
  2. import (
  3. "net/http"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/", func(c flamego.Context) {
  9. c.Redirect("/signup")
  10. })
  11. f.Get("/login", func(c flamego.Context) {
  12. c.Redirect("/signin", http.StatusMovedPermanently)
  13. })
  14. f.Run()
  15. }
  1. $ curl -i http://localhost:2830/
  2. HTTP/1.1 302 Found
  3. ...
  4. $ curl -i http://localhost:2830/login
  5. HTTP/1.1 301 Moved Permanently
  6. ...

WARNING

Be aware that the Redirect method does a naive redirection and is vulnerable to the open redirect vulnerabilityCore services - 图5open in new window.

For example, the following is also works as a valid redirection:

  1. c.Redirect("https://www.google.com")

Please make sure to always first validating the user input!

URL parameters

URL parametersCore services - 图6open in new window, also known as “URL query parameters”, or “URL query strings”, are commonly used to pass arguments to the backend for all HTTP methods (POST forms have to be sent with POST method, as the counterpart).

The Query method is built as a helper for accessing the URL parameters, and return an empty string if no such parameter found with the given key:

  • Code
  • Test
  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Get("/", func(c flamego.Context) string {
  8. return "The name is " + c.Query("name")
  9. })
  10. f.Run()
  11. }
  1. $ curl http://localhost:2830?name=joe
  2. The name is joe
  3. $ curl http://localhost:2830
  4. The name is

There is a family of Query methods available at your fingertips, including:

  • QueryTrim trims spaces and returns value.
  • QueryStrings returns a list of strings.
  • QueryUnescape returns unescaped query result.
  • QueryBool returns value parsed as bool.
  • QueryInt returns value parsed as int.
  • QueryInt64 returns value parsed as int64.
  • QueryFloat64 returns value parsed as float64.

All of these methods accept an optional second argument as the default value when the parameter is absent.

TIP

If you are not happy with the functionality that is provided by the family of Query methods, it is always possible to build your own helpers (or middlware) for the URL parameters by accessing the underlying url.ValuesCore services - 图7open in new window directly:

  1. vals := c.Request().URL.Query()

Is flamego.Context a replacement to context.Context?

No.

The flamego.Context is a representation of the request context and should live within the routing layer, where the context.Context is a general purpose context and can be propogated to almost anywhere (e.g. database layer).

You can retrieve the context.Context of a request using the following methods:

  1. f.Get(..., func(c flamego.Context) {
  2. ctx := c.Request().Context()
  3. ...
  4. })
  5. // or
  6. f.Get(..., func(r *http.Request) {
  7. ctx := r.Context()
  8. ...
  9. })

Default logger

The CharmCore services - 图8open in new window‘s log.LoggerCore services - 图9open in new window is available to all handers for general-purpose structured logging, this is particularly useful if you’re writing middleware:

  1. package main
  2. import (
  3. "github.com/charmbracelet/log"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Get("/", func(r *http.Request, logger log.Logger) {
  9. logger.Info("Hello, Flamego!", "path", r.RequestURI)
  10. })
  11. f.Run()
  12. }

When you run the above program and do curl http://localhost:2830/, the following logs are printed to your terminal:

  1. 2023-03-06 20:57:38 🧙 Flamego: Listening on 0.0.0.0:2830 env=development
  2. 2023-03-06 20:57:51 INFO 🧙 Flamego: Hello, Flamego! path=/

The routing logger is taking advantage of this feature to print the duration and status code of requestsCore services - 图10open in new window.

TIP

Prior to 1.8.0, only the *log.LoggerCore services - 图11open in new window from the standard library is available as the logger.

Response stream

The response stream of a request is represented by the type http.ResponseWriterCore services - 图12open in new window, you may use it as an argument of your handlers or through the ResponseWriter method of the flamego.Context:

  1. f.Get(..., func(w http.ResponseWriter) {
  2. ...
  3. })
  4. // or
  5. f.Get(..., func(c flamego.Context) {
  6. w := c.ResponseWriter()
  7. ...
  8. })

💡 Did you know?

Not all handlers that are registered for a route are always being invoked, the request context (flamego.Context) stops invoking subsequent handlers when the response status code has been writtenCore services - 图13open in new window by the current handler. This is similar to how the short circuit evaluationCore services - 图14open in new window works.

Request object

The request object is represented by the type *http.RequestCore services - 图15open in new window, you may use it as an argument of your handlers or through the Request().Request field of the flamego.Context:

  1. f.Get(..., func(r *http.Request) {
  2. ...
  3. })
  4. // or
  5. f.Get(..., func(c flamego.Context) {
  6. r := c.Request().Request
  7. ...
  8. })

You may wonder what does c.Request() return in the above example?

Good catch! It returns a thin wrapper of the *http.Request and has the type *flamego.RequestCore services - 图16open in new window, which provides helpers to read the request body:

  1. f.Get(..., func(c flamego.Context) {
  2. body := c.Request().Body().Bytes()
  3. ...
  4. })

Routing logger

TIP

This middleware is automatically registered when you use flamego.ClassicCore services - 图17open in new window to create a Flame instance.

The flamego.LoggerCore services - 图18open in new window is the middleware that provides logging of requested routes and corresponding status code:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Logger())
  8. f.Get("/", func() (int, error) {
  9. return http.StatusOK, nil
  10. })
  11. f.Run()
  12. }

When you run the above program and do curl http://localhost:2830/, the following logs are printed to your terminal:

  1. 2023-03-06 20:59:58 🧙 Flamego: Listening on 0.0.0.0:2830 env=development
  2. 2023-03-06 21:00:01 🧙 Flamego: Started method=GET path=/ remote=127.0.0.1
  3. 2023-03-06 21:00:01 🧙 Flamego: Completed method=GET path=/ status=0 duration="564.792µs"

Panic recovery

TIP

This middleware is automatically registered when you use flamego.ClassicCore services - 图19open in new window to create a Flame instance.

The flamego.RecoveryCore services - 图20open in new window is the middleware that is for recovering from panic:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Recovery())
  8. f.Get("/", func() {
  9. panic("I can't breath")
  10. })
  11. f.Run()
  12. }

When you run the above program and visit http://localhost:2830/Core services - 图21open in new window, the recovered page is displayed:

panic recovery

Serving static files

TIP

This middleware is automatically registered when you use flamego.ClassicCore services - 图23open in new window to create a Flame instance.

The flamego.StaticCore services - 图24open in new window is the middleware that is for serving static files, and it accepts an optional flamego.StaticOptionsCore services - 图25open in new window:

  1. func main() {
  2. f := flamego.New()
  3. f.Use(flamego.Static(
  4. flamego.StaticOptions{
  5. Directory: "public",
  6. },
  7. ))
  8. f.Run()
  9. }

You may also omit passing the options for using all default values:

  1. func main() {
  2. f := flamego.New()
  3. f.Use(flamego.Static())
  4. f.Run()
  5. }

Example: Serving the source file

In this example, we’re going to treat our source code file as the static resources:

  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Static(
  8. flamego.StaticOptions{
  9. Directory: "./",
  10. Index: "main.go",
  11. },
  12. ))
  13. f.Run()
  14. }

On line 11, we changed the Directory to be the working directory ("./") instead of the default value "public".

On line 12, we changed the index file (the file to be served when listing a directory) to be main.go instead of the default value "index.html".

When you save the above program as main.go and run it, both curl http://localhost:2830/ and curl http://localhost:2830/main.go will response the content of this main.go back to you.

Example: Serving multiple directories

In this example, we’re going to serve static resources for two different directories.

Here is the setup for the example:

  • Directory
  • css/main.css
  • js/main.js
  • main.go
  • Test
  1. $ tree .
  2. .
  3. ├── css
  4. └── main.css
  5. ├── go.mod
  6. ├── go.sum
  7. ├── js
  8. └── main.js
  9. └── main.go
  1. html {
  2. color: red;
  3. }
  1. console.log("Hello, Flamego!");
  1. package main
  2. import (
  3. "github.com/flamego/flamego"
  4. )
  5. func main() {
  6. f := flamego.New()
  7. f.Use(flamego.Static(
  8. flamego.StaticOptions{
  9. Directory: "js",
  10. },
  11. ))
  12. f.Use(flamego.Static(
  13. flamego.StaticOptions{
  14. Directory: "css",
  15. },
  16. ))
  17. f.Run()
  18. }
  1. $ curl http://localhost:2830/main.css
  2. html {
  3. color: red;
  4. }
  5. $ curl http://localhost:2830/main.js
  6. console.log("Hello, Flamego!");

You may have noticed that the client should not include the value of Directory, which are "css" and "js" in the example. If you would like the client to include these values, you can use the Prefix option:

  1. f.Use(flamego.Static(
  2. flamego.StaticOptions{
  3. Directory: "css",
  4. Prefix: "css",
  5. },
  6. ))

💡 Did you know?

The value of Prefix does not have to be the same as the value of Directory.

Example: Serving the embed.FS

In this example, we’re going to serve static resources from the embed.FSCore services - 图26open in new window that was introduced in Go 1.16Core services - 图27open in new window.

Here is the setup for the example:

  • Directory
  • css/main.css
  • main.go
  • Test
  1. tree .
  2. .
  3. ├── css
  4. └── main.css
  5. ├── go.mod
  6. ├── go.sum
  7. └── main.go
  1. html {
  2. color: red;
  3. }
  1. package main
  2. import (
  3. "embed"
  4. "net/http"
  5. "github.com/flamego/flamego"
  6. )
  7. //go:embed css
  8. var css embed.FS
  9. func main() {
  10. f := flamego.New()
  11. f.Use(flamego.Static(
  12. flamego.StaticOptions{
  13. FileSystem: http.FS(css),
  14. },
  15. ))
  16. f.Run()
  17. }
  1. $ curl http://localhost:2830/css/main.css
  2. html {
  3. color: red;
  4. }

WARNING

Because the Go embed encodes the entire path (i.e. including parent directories), the client have to use the full path, which is different from serving static files directly from the local disk.

In other words, the following command will not work for the example:

  1. $ curl http://localhost:2830/main.css
  2. 404 page not found

Rendering content

The flamego.RendererCore services - 图28open in new window is a minimal middleware that is for rendering content, and it accepts an optional flamego.RenderOptionsCore services - 图29open in new window.

The service flamego.RenderCore services - 图30open in new window is injected to your request context and you can use it to render JSON, XML, binary and plain text content:

  • Code
  • Test
  1. package main
  2. import (
  3. "net/http"
  4. "github.com/flamego/flamego"
  5. )
  6. func main() {
  7. f := flamego.New()
  8. f.Use(flamego.Renderer(
  9. flamego.RenderOptions{
  10. JSONIndent: " ",
  11. },
  12. ))
  13. f.Get("/", func(r flamego.Render) {
  14. r.JSON(http.StatusOK,
  15. map[string]interface{}{
  16. "id": 1,
  17. "username": "joe",
  18. },
  19. )
  20. })
  21. f.Run()
  22. }
  1. $ curl -i http://localhost:2830/
  2. HTTP/1.1 200 OK
  3. Content-Type: application/json; charset=utf-8
  4. ...
  5. {
  6. "id": 1,
  7. "username": "joe"
  8. }

TIP

Try changing the line 13 to JSONIndent: "",, then redo all test requests and see what changes.