Client

Hertz client related functions.

Quick Start

package main

import (
	"context"
	"fmt"
	
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/protocol"
	"github.com/cloudwego/hertz/pkg/protocol/consts"
)

func performRequest() {
	c, _ := client.NewClient()
	req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
	req.SetRequestURI("http://localhost:8080/hello")

	req.SetMethod("GET")
	_ = c.Do(context.Background(), req, resp)
	fmt.Printf("get response: %s\n", resp.Body()) // status == 200 resp.Body() == []byte("hello hertz")
}

func main() {
	h := server.New(server.WithHostPorts(":8080"))
	h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
		ctx.JSON(consts.StatusOK, "hello hertz")
	})
	go performRequest()
	h.Spin()
}

Client Config

Option Default Description
WithDialTimeout 1s dial timeout.
WithMaxConnsPerHost 512 maximum number of connections per host which may be established.
WithMaxIdleConnDuration 10s max idle connection duration, idle keep-alive connections are closed after this duration.
WithMaxConnDuration 0s max connection duration, keep-alive connections are closed after this duration.
WithMaxConnWaitTimeout 0s maximum duration for waiting for a free connection.
WithKeepAlive true determines whether use keep-alive connection, default use.
WithClientReadTimeout 0s maximum duration for full response reading (including body).
WithTLSConfig nil tlsConfig to create a tls connection, for specific configuration information, please refer to tls.
WithDialer network.Dialer specific dialer.
WithResponseBodyStream false determine whether read body in stream or not, default not read in stream.
WithDisableHeaderNamesNormalizing false whether disable header names normalizing, default not disabled, for example, cONTENT-lenGTH -> Content-Length.
WithName "" set client name which used in User-Agent Header.
WithNoDefaultUserAgentHeader false whether default no User-Agent header, default with User-Agent header.
WithDisablePathNormalizing false whether disable path normalizing, default specification path, for example, http://localhost:8080/hello/../ hello -> http://localhost:8080/hello.
WithRetryConfig nil retry configuration, for specific configuration information, please refer to retry.
WithWriteTimeout 0s write timeout.
WithConnStateObserve nil, 5s set function to observe and record the connection status of HTTP client, as well as observe execution intervals.
WithDialFunc network.Dialer set dialer function.
WithHostClientConfigHook nil Set the hook function for re-configure the host client.

Sample Code:

func main() {
	observeInterval := 10 * time.Second
	stateFunc := func(state config.HostClientState) {
		fmt.Printf("state=%v\n", state.ConnPoolState().Addr)
	}
	var customDialFunc network.DialFunc = func(addr string) (network.Conn, error) {
		return nil, nil
	}
	c, err := client.NewClient(
		client.WithDialTimeout(1*time.Second),
		client.WithMaxConnsPerHost(1024),
		client.WithMaxIdleConnDuration(10*time.Second),
		client.WithMaxConnDuration(10*time.Second),
		client.WithMaxConnWaitTimeout(10*time.Second),
		client.WithKeepAlive(true),
		client.WithClientReadTimeout(10*time.Second),
		client.WithDialer(standard.NewDialer()),
		client.WithResponseBodyStream(true),
		client.WithDisableHeaderNamesNormalizing(true),
		client.WithName("my-client"),
		client.WithNoDefaultUserAgentHeader(true),
		client.WithDisablePathNormalizing(true),
		client.WithRetryConfig(
			retry.WithMaxAttemptTimes(3),
			retry.WithInitDelay(1000),
			retry.WithMaxDelay(10000),
			retry.WithDelayPolicy(retry.DefaultDelayPolicy),
			retry.WithMaxJitter(1000),
		),
		client.WithWriteTimeout(10*time.Second),
		client.WithConnStateObserve(stateFunc, observeInterval),
		client.WithDialFunc(customDialFunc, netpoll.NewDialer()),
		client.WithHostClientConfigHook(func(hc interface{}) error {
			if hct, ok := hc.(*http1.HostClient); ok {
				hct.Addr = "FOO.BAR:443"
			}
			return nil
		})
	)
	if err != nil {
		return
	}

	status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
	fmt.Printf("status=%v body=%v\n", status, string(body))
}

Client Request Config

Option Default Description
WithDialTimeout 0s Dial timeout time, this configuration item has a higher priority than the client configuration, which will overwrite the corresponding client configuration item.
WithReadTimeout 0s The maximum duration of a complete read response (including body), this configuration item has a higher priority than the client configuration, which will overwrite the corresponding client configuration item.
WithWriteTimeout 0s HTTP client write timeout, this configuration item has a higher priority than the client configuration, which will overwrite the corresponding client configuration item.
WithRequestTimeout 0s The timeout for a complete HTTP request.
WithTag make(map[string]string) Set the tags field in the form of key-value, used in conjunction with service discovery, details can be found in WithTag.
WithSD false Used in conjunction with service discovery, this request uses service discovery when true is passed, details can be found in WithSD.

Sample Code:

func main() {
	cli, err := client.NewClient()
	if err != nil {
		return
	}
	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetOptions(config.WithDialTimeout(1*time.Second),
		config.WithReadTimeout(3*time.Second),
		config.WithWriteTimeout(3*time.Second),
		config.WithReadTimeout(5*time.Second),
		config.WithSD(true),
		config.WithTag("tag", "tag"))
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://www.example.com")
	err = cli.Do(context.Background(), req, res)
	fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
}

Send Request

func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error
func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)

Do

The Do function executes the given http request and populates the given http response. The request must contain at least one non-zero RequestURI containing the full URL or a non-zero Host header + RequestURI.

This function does not follow redirects. Please use the Get function, DoRedirects function, or Post function to follow the redirection.

If resp is nil, the response will be ignored. If all DefaultMaxConnsPerHost connections against the requesting host are busy, an ErrNoFreeConns error will be returned. In performance-critical code, it is recommended that req and resp be obtained via AcquireRequest and AcquireResponse.

Function Signature:

func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error 

Sample Code:

func main() {
	// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
	c, err := client.NewClient()
	if err != nil {
		return
	}

	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://localhost:8080/ping")

	err = c.Do(context.Background(), req, res)
	fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
	// resp.Body() == []byte("pong") err == <nil>
}

DoRedirects

The DoRedirects function executes the given http request and populates the given http response, following a maximum of maxRedirectsCount redirects. When the number of redirects exceeds maxRedirectsCount, an ErrTooManyRedirects error is returned.

Function Signature:

func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error

Sample Code:

func main() {
	// hertz server
	// http://localhost:8080/redirect ctx.Redirect(consts.StatusMovedPermanently, []byte("/redirect2"))
	// http://localhost:8080/redirect2 ctx.Redirect(consts.StatusMovedPermanently, []byte("/redirect3"))
	// http://localhost:8080/redirect3 ctx.String(consts.StatusOK, "pong")

	c, err := client.NewClient()
	if err != nil {
		return
	}

	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://localhost:8080/redirect")

	err = c.DoRedirects(context.Background(), req, res, 1)
	fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
	// res.Body() == []byte("") err.Error() == "too many redirects detected when doing the request"

	err = c.DoRedirects(context.Background(), req, res, 2)
	fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
	// res.Body() == []byte("pong") err == <nil>
}

Get

The Get function returns the status code of the URL and the response body. If dst is too small, it will be replaced by the response body and returned, otherwise a new slice will be assigned.

The function will automatically follow the redirect.

Function Signature:

func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)

Sample Code:

func main() {
	// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
	c, err := client.NewClient()
	if err != nil {
		return
	}
	status, body, err := c.Get(context.Background(), nil, "http://localhost:8080/ping")
	fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
	// status == 200 res.Body() == []byte("pong") err == <nil>
}

Post

The Post function sends a POST request to the specified URL using the given POST parameters. If dst is too small, it will be replaced by the response body and returned, otherwise a new slice will be assigned.

The function will automatically follow the redirect.

If postArgs is nil, then an empty POST request body is sent.

Function Signature:

func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)

Sample Code:

func main() {
	// hertz server:http://localhost:8080/hello ctx.String(consts.StatusOK, "hello %s", ctx.PostForm("name"))
	c, err := client.NewClient()
	if err != nil {
		return
	}

	var postArgs protocol.Args
	postArgs.Set("name", "cloudwego") // Set post args
	status, body, err := c.Post(context.Background(), nil, "http://localhost:8080/hello", &postArgs)
	fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
	// status == 200 res.Body() == []byte("hello cloudwego") err == <nil>
}

Request Timeout

Note: Do, DoRedirects, Get, Post, and other request functions can set the request timeout time through WithRequestTimeout. The DoTimeout and DoDeadline functions set the request timeout time through parameter passing. Both modify the RequestOptions.requestTimeout field, so there is no need to use the WithRequestTimeout function when using the DoTimeout and DoDeadline functions, the request timeout time is based on the last setting.

func WithRequestTimeout(t time.Duration) RequestOption
func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error
func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error

WithRequestTimeout

Although the Do, DoRedirects, Get, Post function cannot set the request timeout by passing parameters, it can be set through the WithRequestTimeout configuration item in the Client Request Configuration.

Sample Code:

func main() {
	c, err := client.NewClient()
	if err != nil {
		return
	}

	// Do
	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetOptions(config.WithRequestTimeout(5 * time.Second))
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://localhost:8888/get")
	err = c.Do(context.Background(), req, res)

	// DoRedirects
	err = c.DoRedirects(context.Background(), req, res, 5)

	// Get
	_, _, err = c.Get(context.Background(), nil, "http://localhost:8888/get", config.WithRequestTimeout(5*time.Second))

	// Post
	postArgs := &protocol.Args{}
	_, _, err = c.Post(context.Background(), nil, "http://localhost:8888/post", postArgs, config.WithRequestTimeout(5*time.Second))
}

DoTimeout

The DoTimeout function executes the given request and waits for a response within the given timeout period.

This function does not follow redirects. Please use the Get function, DoRedirects function, or Post function to follow the redirection.

If resp is nil, the response is ignored. If the response is not received within the given timeout period, an errTimeout error is returned.

Function Signature:

func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error

Sample Code:

func main() {
	// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong") biz handler time: 1.5s
	c, err := client.NewClient()
	if err != nil {
		return
	}

	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://localhost:8080/ping")

	err = c.DoTimeout(context.Background(), req, res, time.Second*3)
	fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
	// res.Body() == []byte("pong") err == <nil>

	err = c.DoTimeout(context.Background(), req, res, time.Second)
	fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
	// res.Body() == []byte("") err.Error() == "timeout"
}

DoDeadline

DoDeadline executes the given request and waits for the response until the given deadline.

This function does not follow redirects. Please use the Get function, DoRedirects function, or Post function to follow the redirection.

If resp is nil, the response is ignored. If the response is not received by the given deadline, an errTimeout error is returned.

Function Signature:

func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error

Sample Code:

func main() {
	// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong") biz handler time: 1.5s
	c, err := client.NewClient()
	if err != nil {
		return
	}

	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://localhost:8080/ping")

	err = c.DoDeadline(context.Background(), req, res, time.Now().Add(3*time.Second))
	fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
	// res.Body() == []byte("pong") err == <nil>

	err = c.DoDeadline(context.Background(), req, res, time.Now().Add(1*time.Second))
	fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
	// res.Body() == []byte("") err.Error() == "timeout"
}

Request Retry

func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)

SetRetryIfFunc

The SetRetryIfFunc method is used to customize the conditions under which retry occurs. (For more information, please refer to retry-condition-configuration)

Function Signature:

func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)

Sample Code:

func main() {
	c, err := client.NewClient()
	if err != nil {
		return
	}
	var customRetryIfFunc = func(req *protocol.Request, resp *protocol.Response, err error) bool {
		return true
	}
	c.SetRetryIfFunc(customRetryIfFunc)
	status2, body2, _ := c.Get(context.Background(), nil, "http://www.example.com")
	fmt.Printf("status=%v body=%v\n", status2, string(body2))
}

Add Request Content

Hertz’s client can add various forms of request content in HTTP requests, such as query parameters, www url encoded, multipart/form data, and JSON.

Sample Code:

func main() {
	client, err := client.NewClient()
	if err != nil {
		return
	}
	req := &protocol.Request{}
	res := &protocol.Response{}

	// Use SetQueryString to set query parameters
	req.Reset()
	req.Header.SetMethod(consts.MethodPost)
	req.SetRequestURI("http://127.0.0.1:8080/v1/bind")
	req.SetQueryString("query=query&q=q1&q=q2&vd=1")
	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}

	// Send "www-url-encoded" request
	req.Reset()
	req.Header.SetMethod(consts.MethodPost)
	req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
	req.SetFormData(map[string]string{
		"form": "test form",
	})
	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}

	// Send "multipart/form-data" request
	req.Reset()
	req.Header.SetMethod(consts.MethodPost)
	req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
	req.SetMultipartFormData(map[string]string{
		"form": "test form",
	})
	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}

	// Send "Json" request
	req.Reset()
	req.Header.SetMethod(consts.MethodPost)
	req.Header.SetContentTypeBytes([]byte("application/json"))
	req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
	data := struct {
		Json string `json:"json"`
	}{
		"test json",
	}
	jsonByte, _ := json.Marshal(data)
	req.SetBody(jsonByte)
	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}
}

Upload File

Hertz’s client supports uploading files to the server.

Sample Code:

func main() {
	client, err := client.NewClient()
	if err != nil {
		return
	}
	req := &protocol.Request{}
	res := &protocol.Response{}
	req.SetMethod(consts.MethodPost)
	req.SetRequestURI("http://127.0.0.1:8080/singleFile")
	req.SetFile("file", "your file path")

	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}
	fmt.Println(err, string(res.Body()))
}

Streaming Read Response Content

Hertz’s client supports streaming read HTTP response content.

Since the client has the problem of multiplexing connections, if streaming is used, the connection will be handled by the user(resp.BodyStream() is encapsulated by connection) once streaming is used. There are some differences in the management of connections in the above case:

  1. If the user doesn’t close the connection, the connection will eventually be closed by the GC without causing a connection leak. However, due to the need to wait for 2 Round-Trip Time to close the connection, in the case of high concurrency, the consequence is that there will be too many open files and creating a new connection will be impossible.

  2. Users can recycle the connection by calling the relevant interface. After recycling, the connection will be put into the connection pool for reuse, so as to achieve higher resource utilization and better performance. The following methods will recycle the connection. Warning: Recycling can only be done once.

    1. Explicit call: protocol.ReleaseResponse(), resp.Reset(), resp.ResetBody().
    2. Implicit call: The server side will also recycle the response. If the client and server use the same response, there is no need to explicitly call the recycling method

Sample Code:

func main() {
	c, _ := client.NewClient(client.WithResponseBodyStream(true))
	req := &protocol.Request{}
	resp := &protocol.Response{}
	defer func() {
		protocol.ReleaseRequest(req)
		protocol.ReleaseResponse(resp)
	}()
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("http://127.0.0.1:8080/streamWrite")
	err := c.Do(context.Background(), req, resp)
	if err != nil {
		return
	}
	bodyStream := resp.BodyStream()
	p := make([]byte, resp.Header.ContentLength()/2)
	_, err = bodyStream.Read(p)
	if err != nil {
		fmt.Println(err.Error())
	}
	left, _ := ioutil.ReadAll(bodyStream)
	fmt.Println(string(p), string(left))
}

Service Discovery

The Hertz client supports finding target servers through service discovery.

Hertz supports custom service discovery modules. For more information, please refer to service-discovery-extension.

The relevant content of the service discovery center that Hertz has currently accessed can be found in Service Registration and Service Discovery Extensions.

TLS

The network library netpoll used by Hertz client by default does not support TLS. If you want to configure TLS to access https addresses, you should use the Standard library.

For TLS related configuration information, please refer to tls.

Sample Code:

func main() {
	clientCfg := &tls.Config{
		InsecureSkipVerify: true,
	}
	c, err := client.NewClient(
		client.WithTLSConfig(clientCfg),
		client.WithDialer(standard.NewDialer()),
	)
	if err != nil {
		return
	}
	
	req, res := &protocol.Request{}, &protocol.Response{}
	req.SetMethod(consts.MethodGet)
	req.SetRequestURI("https://www.example.com")

	err = c.Do(context.Background(), req, res)
	fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
}

Forward Proxy

func (c *Client) SetProxy(p protocol.Proxy)

SetProxy

SetProxy is used to set the client proxy. (For more information, please refer to retry-condition-configuration)

Note: Multiple proxies cannot be set for the same client. If you need to use another proxy, please create another client and set a proxy for it.

Sample Code:

func (c *Client) SetProxy(p protocol.Proxy)

Function Signature:

func main() {
	// Proxy address
	proxyURL := "http://<__user_name__>:<__password__>@<__proxy_addr__>:<__proxy_port__>"

	parsedProxyURL := protocol.ParseURI(proxyURL)
	client, err := client.NewClient(client.WithDialer(standard.NewDialer()))
	if err != nil {
		return
	}
	client.SetProxy(protocol.ProxyURI(parsedProxyURL))
	upstreamURL := "http://google.com"
	_, body, _ := client.Get(context.Background(), nil, upstreamURL)
	fmt.Println(string(body))
}

Close Idle Connections

func (c *Client) CloseIdleConnections()

CloseIdleConnections

The CloseIdleConnections method is used to close any keep-alive connections that are in an idle state. These connections may have been established by a previous request, but have been idle for some time now. This method does not break any connections that are currently in use.

Function Signature:

func (c *Client) CloseIdleConnections()

Sample Code:

func main() {
    c, err := client.NewClient()
    if err != nil {
        return
    }
    status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
    fmt.Printf("status=%v body=%v\n", status, string(body))

    // close idle connections
    c.CloseIdleConnections()
}

Get Dialer Name

func (c *Client) GetDialerName() (dName string, err error)

GetDialerName

The GetDialerName method is used to get the name of the dialer currently used by the client. If the dialer name cannot be retrieved, unknown is returned.

Function Signature:

func (c *Client) GetDialerName() (dName string, err error)

Sample Code:

func main() {
	c, err := client.NewClient()
	if err != nil {
		return
	}
	// get dialer name
	dName, err := c.GetDialerName()
	if err != nil {
		fmt.Printf("GetDialerName failed: %v", err)
		return
	}
	fmt.Printf("dialer name=%v\n", dName)
	// dName == "standard"
}

Middleware

func (c *Client) Use(mws ...Middleware)
func (c *Client) UseAsLast(mw Middleware) error
func (c *Client) TakeOutLastMiddleware() Middleware

Use

Use the Use method to add a middleware to the current client. (For more information, please refer to client-side-middleware)

Function Signature:

func (c *Client) Use(mws ...Middleware)

UseAsLast

The UseAsLast function adds the middleware to the end of the client middleware chain.

If the client middleware chain has already set the last middleware before, the UseAsLast function will return an errorLastMiddlewareExist error. Therefore, to ensure that the last middleware in the client middleware chain is empty, you can first use the TakeOutLastMiddleware function to clear the last middleware in the client middleware chain.

Note: The UseAsLast function sets the middleware in c.lastMiddleware, while the middleware chain set using the Use function is stored in c.mws. The two functions are relatively independent. c.lastMiddleware is executed only at the end of the client middleware chain. Therefore, the UseAsLast function can be called before or after the Use function.

Function Signature:

func (c *Client) UseAsLast(mw Middleware) error

Sample Code:

func main() {
	client, err := client.NewClient()
	if err != nil {
		return
	}
	client.Use(MyMiddleware)
	client.UseAsLast(LastMiddleware)
	req := &protocol.Request{}
	res := &protocol.Response{}
	req.SetRequestURI("http://www.example.com")
	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}
}

TakeOutLastMiddleware

The TakeOutLastMiddleware function returns the last middleware set in the UseAsLast function and clears it. If it is not set, it returns nil.

Function Signature:

func (c *Client) TakeOutLastMiddleware() Middleware

Sample Code:

func main() {
	client, err := client.NewClient()
	if err != nil {
		return
	}
	client.Use(MyMiddleware)
	client.UseAsLast(LastMiddleware)
	req := &protocol.Request{}
	res := &protocol.Response{}
	req.SetRequestURI("http://www.example.com")
	err = client.Do(context.Background(), req, res)
	if err != nil {
		return
	}
	middleware := client.TakeOutLastMiddleware() // middleware == LastMiddleware
	middleware = client.TakeOutLastMiddleware() // middleware == nil
}

Last modified January 18, 2024 : Upload volo blog (#936) (1fc8abb)