Golang Reverse Proxy Practices
Tags: webdev, beginners, go, programming
Published: 2022-11-25 10:29:29

## Hertz

[Hertz](https://github.com/cloudwego/hertz) is an ultra-large-scale enterprise-level microservice HTTP framework, featuring high ease of use, easy expansion, and low latency etc.

Hertz uses the self-developed high-performance network library Netpoll by default. In some special scenarios, Hertz has certain advantages in QPS and latency compared to go net.

In internal practice, some typical services, such as services with a high proportion of frameworks, gateways and other services, after migrating to Hertz, compared to the Gin framework, the resource usage is significantly reduced, **CPU** **usage is reduced by 30%-60% with the size of the traffic**.

> For more details, see [cloudwego/hertz](https://github.com/cloudwego/hertz).

## Reverse proxy

In computer networks, a reverse proxy is an application that sits in front of back-end applications and forwards client (e.g. browser) requests to those applications.

Reverse proxies help increase scalability, performance, resilience and security. The resources returned to the client appear as if they originated from the web server itself.

## Using reverse proxy in Hertz

Using a reverse proxy with Hertz requires pulling the [reverseproxy](https://github.com/hertz-contrib/reverseproxy) extension provided by the community.

```Shell
$ go get github.com/hertz-contrib/reverseproxy
```

### Basic using

```Go
package main

import (
        "context"

        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8000"))
        proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
        if err != nil {
                panic(err)
        }
        h.GET("/proxy/backend", func(cc context.Context, c *app.RequestContext) {
                c.JSON(200, utils.H{
                        "msg": "proxy success!!",
                })
        })
        h.GET("/backend", proxy.ServeHTTP)
        h.Spin()
}
```

We set the target path of the reverse proxy `/proxy` through the `NewSingleHostReverseProxy` function. Next, the path of the registered route is the sub-path `/proxy/backend` of the target path of the reverse proxy, and finally the reverse proxy service `proxy.ServeHTTP` is mapped by registering `/backend`. In this way, when we access `/backend` through the GET method, we will access the content in `/proxy/backend`.

```Shell
curl 127.0.0.1:8000/backend

{"msg":"proxy success!!"}
```

### Custom configuration

Of course, the extension is not only able to implement a simple reverse proxy, there are many customizable options provided in the reverseproxy extension.

| Method              | Description                               |
| ------------------- | ----------------------------------------- |
| `SetDirector`       | use to customize protocol.Request         |
| `SetClient`         | use to customize client                   |
| `SetModifyResponse` | use to customize modify response function |
| `SetErrorHandler`   | use to customize error handler            |

#### SetDirector & SetClient

We practice using `SetDirector` and `SetClient` by implementing a simple service registration discovery.

##### Server

```Go
package main

import (
        "context"

        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/app/server/registry"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/cloudwego/hertz/pkg/protocol/consts"
        "github.com/hertz-contrib/registry/nacos"
)

func main() {
        addr := "127.0.0.1:8000"
        r, _ := nacos.NewDefaultNacosRegistry()
        h := server.Default(
                server.WithHostPorts(addr),
                server.WithRegistry(r, &registry.Info{
                        ServiceName: "demo.hertz-contrib.reverseproxy",
                        Addr:        utils.NewNetAddr("tcp", addr),
                        Weight:      10,
                }),
        )
        h.GET("/backend", func(cc context.Context, c *app.RequestContext) {
                c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
        })
        h.Spin()
}
```

The sample code on the server side in the [hertz-contrib/registry](https://github.com/hertz-contrib/registry) extension is used here. Since this is not the main content of this article, it will not be expanded. For more information, you can Go to the registry repository.

##### Client

```Go
package main

import (
        "github.com/cloudwego/hertz/pkg/app/client"
        "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/config"
        "github.com/cloudwego/hertz/pkg/protocol"
        "github.com/hertz-contrib/registry/nacos"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        cli, err := client.NewClient()
        if err != nil {
                panic(err)
        }
        r, err := nacos.NewDefaultNacosResolver()
        if err != nil {
                panic(err)
        }
        cli.Use(sd.Discovery(r))
        h := server.New(server.WithHostPorts(":8741"))
        proxy, _ := reverseproxy.NewSingleHostReverseProxy("http://demo.hertz-contrib.reverseproxy")
        proxy.SetClient(cli)
        proxy.SetDirector(func(req *protocol.Request) {
                req.SetRequestURI(string(reverseproxy.JoinURLPath(req, proxy.Target)))
                req.Header.SetHostBytes(req.URI().Host())
                req.Options().Apply([]config.RequestOption{config.WithSD(true)})
        })
        h.GET("/backend", proxy.ServeHTTP)
        h.Spin()
}
```

In the Client section we used a reverse proxy for service discovery. First, specify the client using the service discovery middleware as our forwarding client through `SetClient`, and then use `SetDirector` to specify our protocol.Request , and configure the use of service discovery in the new Request.

#### SetModifyResponse & SetErrorHandler

`SetModifyResponse` and `SetErrorHandler` respectively set the response from the backend and the handling of errors arriving in the backend. `SetModifyResponse` is actually `modifyResponse` in the reverse proxy extension. If the backend returns any response, no matter what the status code is, this method will be called. If the `modifyResponse` method returns an error, the `errorHandler` method will be called with the error as an argument.

##### SetModifyResponse

```Go
package main

import (
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/protocol"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8000"))
        // modify response
        proxy, _ := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
        proxy.SetModifyResponse(func(resp *protocol.Response) error {
                resp.SetStatusCode(200)
                resp.SetBodyRaw([]byte("change response success"))
                return nil
        })
        h.GET("/modifyResponse", proxy.ServeHTTP)

        h.Spin()
}
```

Here, `modifyResponse` is modified by `SetModifyResponse` to change the processing content of the response.

**Test**

```Go
curl 127.0.0.1:8000/modifyResponse

change response success
```

##### SetErrorHandler

```Go
package main

import (
        "context"

        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8002"))
        proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
        if err != nil {
                panic(err)
        }
        proxy.SetErrorHandler(func(c *app.RequestContext, err error) {
                c.Response.SetStatusCode(404)
                c.String(404, "fake 404 not found")
        })

        h.GET("/proxy/backend", func(cc context.Context, c *app.RequestContext) {
                c.JSON(200, utils.H{
                        "msg": "proxy success!!",
                })
        })
        h.GET("/backend", proxy.ServeHTTP)
        h.Spin()
}
```

We use `SetErrorHandler` to specify how to handle errors that arrive in the background. When an error arrives in the background or there is an error from `modifyResponse`, the specified processing logic will be run.

**Test**

```Go
curl 127.0.0.1:8002/backend

fake 404 not found
```

### Middleware usage

In addition to basic use, Hertz reverse proxy also supports use in middleware.

```Go
package main

import (
        "context"
        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        r := server.Default(server.WithHostPorts("127.0.0.1:9998"))
        r2 := server.Default(server.WithHostPorts("127.0.0.1:9997"))

        proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:9997")
        if err != nil {
                panic(err)
        }

        r.Use(func(c context.Context, ctx *app.RequestContext) {
                if ctx.Query("country") == "cn" {
                        proxy.ServeHTTP(c, ctx)
                        ctx.Response.Header.Set("key", "value")
                        ctx.Abort()
                } else {
                        ctx.Next(c)
                }
        })

        r.GET("/backend", func(c context.Context, ctx *app.RequestContext) {
                ctx.JSON(200, utils.H{
                        "message": "pong1",
                })
        })

        r2.GET("/backend", func(c context.Context, ctx *app.RequestContext) {
                ctx.JSON(200, utils.H{
                        "message": "pong2",
                })
        })

        go r.Spin()
        r2.Spin()
}
```

In this example code, two Hertz instances are initialized first, then `NewSingleHostReverseProxy` is used to set the reverse proxy target to port 9997, and finally two routes with the same path are registered for the two instances respectively.

**Test1**

```Shell
curl 127.0.0.1:9997/backend

{"message":"pong2"}
curl 127.0.0.1:9998/backend

{"message":"pong1"}
```

The main part of this code is the middleware usage part. We use the middleware through `Use`. In the middleware logic, when the logic of `ctx.Query("country") == "cn"` is established, call `proxy. ServeHTTP(c, ctx)` uses a reverse proxy, and when you request `/backend` through instance r, it will request the content of the reverse proxy target port 9997.

**Test2**

```Shell
curl 127.0.0.1:9998/backend?country=cn

{"message":"pong2"}
```

### Tips

- For `NewSingleHostReverseProxy` function, if no `config.ClientOption` is passed it will use the default global `client.Client` instance. When passing `config.ClientOption` it will initialize a local `client.Client` instance. Using `ReverseProxy.SetClient` if there is need for shared customized `client.Client` instance.
- The reverse proxy resets the header of the response, any such modifications before the request is made will be discarded.

## Reference

- [cloudwego/hertz](https://github.com/cloudwego/hertz)

- [cloudwego/hertz-examples](https://github.com/cloudwego/hertz-examples)

- [hertz-contrib/reverseproxy](https://github.com/cloudwego/hertz)