Skip to content

Commit a396c06

Browse files
authored
optmize: kitex doc (#801)
1 parent 376b20f commit a396c06

File tree

4 files changed

+486
-158
lines changed

4 files changed

+486
-158
lines changed

content/en/docs/kitex/Tutorials/framework-exten/middleware.md

Lines changed: 222 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,259 @@ description: >
66
---
77

88
## Introduction
9+
Kitex, as a lightweight RPC framework, offers powerful extensibility and primarily provides two methods of extension: one is a relatively low-level approach that involves adding middleware directly, and the other is a higher-level approach that involves adding suites. The following mainly introduces the usage of middleware.
910

10-
Middleware is the major method to extend the Kitex framework. Most of the Kitex-based extensions and secondary development features are based on middleware.
11+
## Middleware
12+
Middleware is a relatively low level of extension. Most of the Kitex-based extension and secondary development functions are based on middleware to achieve.
13+
Kitex's Middleware is defined in `pkg/endpoint/endpoint.go`, the most important of which are two types:
1114

12-
Before extending, it is important to remember two principles:
13-
14-
1. Middleware and Suit are only allowed to be set before initializing Server and Client, do not allow modified dynamically.
15-
2. Middlewares are executed in the order in which they were added.
15+
1. `Endpoint` is a function that accepts ctx, req, resp and returns err. Please refer to the following "Example" code.
16+
2. Middleware (hereinafter referred to as MW) is also a function that receives and returns an Endpoint.
17+
```golang
18+
type Middleware func(Endpoint) Endpoint
19+
```
1620

17-
Middleware is defined in `pkg/endpoint/endpoint.go`, the two major types are:
21+
In fact, a middleware is essentially a function that takes an Endpoint as input and returns an Endpoint as output. This ensures transparency to the application, as the application itself is unaware of whether it is being decorated by middleware. Due to this feature, middlewares can be nested and used in combination.
1822

19-
1. `Endpoint` is a function that accepts ctx, req, resp and returns err, see the example below.
20-
2. `Middleware` (aka MW) is also a function that receives and returns an `Endpoint`. 3.
23+
Middlewares are used in a chained manner. By invoking the provided next function, you can obtain the response (if any) and error returned by the subsequent middleware. Based on this, you can perform the necessary processing and return an error to the previous middleware (be sure to check for errors returned by next and avoid swallowing errors) or set the response accordingly.
2124

22-
In fact, a middleware is a function whose input and output are both `Endpoint`, which ensures the transparency to the application, and the application itself does not need to know whether it is decorated by the middleware. Due to this feature, middleware can be nested.
25+
### Client Middleware
2326

24-
Middleware should be used in series, by calling the next, you can get the response (if any) and err returned by the latter middleware, and then process accordingly and return the err to the former middleware (be sure to check the err of next function returned, do not swallow the err) or set the response.
27+
There are two ways to add Client Middleware:
2528

26-
## Client-side Middleware
29+
1. Client.WithMiddleware adds Middleware to the current client, which is executed after service circuit breaker and timeout Middleware ;
30+
2. Client.WithInstanceMW adds middleware to the current client, which is executed after service discovery and load balance. If there is an instance circuit breaker, it will be executed after the instance circuit breaker (if Proxy is used, it will not be called, such as in Mesh mode).
31+
Note that the above functions should all be passed as options when creating the client.
2732

28-
There are two ways to add client-side middleware:
33+
Client Middleware call sequence:
2934

30-
1. `client.WithMiddleware` adds a middleware to the current client, executes after service circuit breaker middleware and timeout middleware.
31-
2. `client.WithInstanceMW` adds a middleware to the current client and executes after service discovery and load balancing. If there has instance circuit breaker, this middleware will execute after instance circuit breaker. (if `Proxy` is used, it will not be called).
35+
1. XDS routing, service level circuit breaker , timeout;
36+
2. ContextMiddleware;
37+
3. Middleware set by Client.WithMiddleware ;
38+
4. ACLMiddleware;
39+
5. Service Discovery , Instance circuit breaker , Instance-Level Middleware/Service Discovery, Proxy Middleware
40+
6. IOErrorHandleMW
3241

33-
Note that the above functions should all be passed as `Option`s when creating the client.
42+
The above can be seen in [https://github.com/cloudwego/kitex/blob/develop/client/client.go](https://github.com/cloudwego/kitex/blob/develop/client/client.go)
3443

35-
The order of client middleware calls:
36-
1. the middleware set by `client.WithMiddleware`
37-
2. ACLMiddleware
38-
3. (ResolveMW + client.WithInstanceMW + PoolMW / DialerMW) / ProxyMW
39-
4. IOErrorHandleMW
44+
### Context Middleware
45+
Context Middleware is essentially Client Middleware, but the difference is that it is controlled by ctx whether and which to inject middleware.
46+
The introduction of Context Middleware is to provide a method that can inject Client Middleware globally or dynamically. Typical usage scenarios include statistics on which downstream interfaces are called.
47+
Middleware can be injected into ctx using `ctx = client.WithContextMiddlewares(ctx, mw)`.
48+
Note: Context Middleware executes before middleware set by `client.WithMiddleware()`.
4049

41-
The order in which the calls are returned is reversed.
50+
### Server Middleware
51+
There are indeed certain differences between server-side middleware and client-side middleware.
52+
You can add server-side middleware through server.WithMiddleware, which is used in the same way as the client and passed in through Option when creating the server.
4253

43-
The order of all middleware calls on the client side can be seen in `client/client.go`.
54+
Server Middleware call sequence:
4455

45-
## Context Middleware
56+
1. ErrHandleMW
57+
2. ACLMiddleware
58+
3. Middleware set by Server.WithMiddleware
4659

47-
Context middleware is also a client-side middleware, but the difference is that it is controlled by ctx whether to inject the middleware or which middleware should be injected.
60+
The above can be seen in https://github.com/cloudwego/kitex/blob/develop/server/server.go
61+
### Example
62+
We can use the following example to see how to use Middleware.
63+
### Request/Reponse
64+
If we need to print out the request content before the request, and then print out the response content after the request, we can write the following middleware:
4865

49-
The introduction of Context Middleware is to provide a way to globally or dynamically inject Client Middleware. Typical usage scenario is to count which downstreams are called in this call-chain.
66+
```golang
67+
/*
68+
type Request struct {
69+
Message string `thrift:"Message,1,required" frugal:"1,required,string" json:"Message"`
70+
Base *base.Base `thrift:"Base,255,optional" frugal:"255,optional,base.Base" json:"Base,omitempty"`
71+
}
5072
51-
Context Middleware only exists in the context call-chain, which can avoid problems caused by third-party libraries injecting uncontrollable middleware.
73+
type Response struct {
74+
Message string `thrift:"Message,1,required" frugal:"1,required,string" json:"Message"`
75+
BaseResp *base.BaseResp `thrift:"BaseResp,255,optional" frugal:"255,optional,base.BaseResp" json:"BaseResp,omitempty"`
76+
}
77+
*/
78+
import "github.com/cloudwego/kitex/pkg/utils"
79+
80+
func ExampleMiddleware(next endpoint.Endpoint) endpoint.Endpoint {
81+
return func(ctx context.Context, request, response interface{}) error {
82+
if arg, ok := request.(utils.KitexArgs); ok {
83+
if req := arg.GetFirstArgument().(*echo.Request; req != nil {
84+
klog.Debugf("Request Message: %v", req.Message)
85+
}
86+
}
87+
err := next(ctx, request, response)
88+
if result, ok := response.(utils.KitexResult); ok {
89+
if resp, ok := result.GetResult().(*echo.Response); ok {
90+
klog.Debugf("Response Message: %v", resp.Message)
91+
// resp.SetSuccess(...) could be used to replace customized response
92+
// But notice: the type should be the same with the response of this method
93+
}
94+
}
95+
return err
96+
}
97+
}
98+
```
99+
The provided example is for illustrative purposes, and it is indeed important to exercise caution when implementing such logging practices in a production environment. Logging every request and response indiscriminately can indeed have performance implications, especially when dealing with large response bodies.
52100

53-
Middleware can be injected into ctx with `ctx = client.WithContextMiddlewares(ctx, mw)` .
101+
### Precautions
102+
If RPCInfo is used in custom middleware, be aware that RPCInfo will be recycled after the rpc ends, so if you use goroutine operation RPCInfo in middleware, there will be issues . Please avoid such operations .
54103

55-
Note: Context Middleware will be executed before Client Middleware.
104+
### gRPC Middleware
105+
As we all know, in addition to Thrift, Kitex also supports the protobuf and gRPC encoding/decoding protocols. In the case of protobuf, it refers to using protobuf exclusively to define the payload format, and the service definition only includes unary methods. However, if streaming methods are introduced, Kitex will use the gRPC protocol for encoding/decoding and communication.
56106

57-
## Server-side Middleware
107+
For services using protobuf (unary only), the development of middleware remains consistent with the previous context, as the design of both is identical.
58108

59-
The server-side middleware is different from the client-side.
109+
However, if streaming methods are used, the development of middleware is completely different. Therefore, the usage of gRPC streaming middleware is explained separately as a standalone unit.
60110

61-
You can add server-side middleware via `server.WithMiddleware`, and passing `Option` when creating the server.
111+
For streaming methods, such as client stream, server stream, bidirectional stream, etc., and considering that the sending and receiving of messages (Recv & Send) have their own business logic control, middleware can not cover the messages themselves. Therefore, if you want to implement request/response logging at the message level during Send/Recv operations, you need to wrap Kitex's streaming.Stream as follows:
62112

63-
The order of server-side middleware calls can be found in `server/server.go`.
113+
```golang
114+
type wrappedStream struct {
115+
streaming.Stream
116+
}
64117

65-
## Example
118+
func (w *wrappedStream) RecvMsg(m interface{}) error {
119+
log.Printf("Receive a message: %T(%v)", m, m)
120+
return w.Stream.RecvMsg(m)
121+
}
66122

67-
You can see how to use the middleware in the following example.
123+
func (w *wrappedStream) SendMsg(m interface{}) error {
124+
log.Printf("Send a message: %T(%v)", m, m)
125+
return w.Stream.SendMsg(m)
126+
}
68127

69-
If you have a requirement to print out the request and the response, we can write the following MW:
128+
func newWrappedStream(s streaming.Stream) streaming.Stream {
129+
return &wrappedStream{s}
130+
}
70131

71-
```go
72-
func PrintRequestResponseMW(next endpoint.Endpoint) endpoint.Endpoint {
73-
return func(ctx context.Context, request, response interface{}) error {
74-
fmt.Printf("request: %v\n", request)
75-
err := next(ctx, request, response)
76-
fmt.Printf("response: %v", response)
77-
return err
132+
```
133+
Then, within the middleware, insert the wrapped streaming.Stream object at specific invocation points.
134+
```golang
135+
import "github.com/cloudwego/kitex/pkg/streaming"
136+
137+
// A middleware that can be used for both client-side and server-side in Kitex with gRPC/Thrift/TTheader-protobuf
138+
func DemoGRPCMiddleware(next endpoint.Endpoint) endpoint.Endpoint {
139+
return func(ctx context.Context, req, res interface{}) error {
140+
141+
var Nil interface{} // can not switch nil directly in go
142+
switch Nil {
143+
case req: // The current middleware is used for the client-side and specifically designed for streaming methods
144+
err := next(ctx, req, res)
145+
// The stream object can only be obtained after the final endpoint returns
146+
if tmp, ok := res.(*streaming.Result); err == nil && ok {
147+
tmp.Stream = newWrappedStream(tmp.Stream) // wrap stream object
148+
}
149+
return err
150+
case res: // The current middleware is used for the server-side and specifically designed for streaming methods
151+
if tmp, ok := req.(*streaming.Args); ok {
152+
tmp.Stream = newWrappedStream(tmp.Stream) // wrap stream object
153+
}
154+
default: // pure unary method, or thrift method
155+
// do something else
156+
}
157+
return next(ctx, req, res)
78158
}
79159
}
80160
```
161+
Explanation of the request/response parameter types obtained within the Kitex middleware in different scenarios of gRPC:
162+
163+
| Scenario | Request Type | Response Type |
164+
|----------------------------------|---------------------------|-----------------------------|
165+
| Kitex-gRPC Server Unary/Streaming | *streaming.Args | nil |
166+
| Kitex-gRPC Client Unary | *xxxservice.XXXMethodArgs | *xxxservice.XXXMethodResult |
167+
| Kitex-gRPC Client Streaming | nil | *streaming.Result |
168+
169+
## Summary
170+
Middleware is indeed a lower-level implementation of extensions, typically used to inject simple code containing specific functionalities. However, in complex scenarios, single middleware may not be sufficient to meet the business requirements. In such cases, a more comprehensive approach is needed, which involves assembling multiple middlewares or options into a complete middleware layer. Users can develop this requirement based on suites, refer to [Suite Extend](https://www.cloudwego.io/zh/docs/kitex/tutorials/framework-exten/suite/)
171+
172+
## FAQ
173+
### How to recover handler panic in middleware
174+
Question:
175+
A handler who wanted to recover their own business in middleware threw a panic and found that the panic had already been recovered by the framework.
176+
177+
Description:
178+
The framework will recover and report the panic in Handler. If you want to capture panic in custom middleware, you can determine the type of error returned in middleware (whether it is `kerrors.ErrPanic`).
179+
```golang
180+
func TestServerMiddleware(next endpoint.Endpoint) endpoint.Endpoint {
181+
return func(ctx context.Context, req, resp interface{}) (err error) {
182+
err = next(ctx, req, resp)
183+
if errors.Is(err, kerrors.ErrPanic) {
184+
fmt.Println("capture panic")
185+
}
186+
return err
187+
}
188+
}
189+
```
190+
### How to get the real Request/Response in Middleware?
191+
192+
Due to implementation needs, the req and resp passed in middlewares are not the req and resp passed by the real user, but an object wrapped by Kitex, specifically a structure similar to the following.
81193

82-
Assuming we are at Server side, we can use `server.WithMiddleware(PrintRequestResponseMW)` to use this MW.
194+
#### Thrift
83195

84-
**The above scenario is only for example, not for production, there will be performance issues. **
196+
```golang
197+
// req
198+
type ${XService}${XMethod}Args struct {
199+
Req *${XRequest} `thrift:"req,1" json:"req"`
200+
}
201+
202+
func (p *${XService}${XMethod}Args) GetFirstArgument() interface{} {
203+
return p.Req
204+
}
205+
206+
207+
// resp
208+
type ${XService}${XMethod}Result struct {
209+
Success *${XResponse} `thrift:"success,0" json:"success,omitempty"`
210+
}
85211

86-
## Attention
212+
func (p *${XService}${XMethod}Result) GetResult() interface{} {
213+
return p.Success
214+
}
215+
```
216+
217+
#### Protobuf
218+
219+
```golang
220+
// req
221+
type ${XMethod}Args struct {
222+
Req *${XRequest}
223+
}
224+
225+
func (p *${XMethod}Args) GetReq() *${XRequest} {
226+
if !p.IsSetReq() {
227+
return ${XMethod}Args_Req_DEFAULT
228+
}
229+
return p.Req
230+
}
231+
232+
233+
// resp
234+
type ${XMethod}Result struct {
235+
Success *${XResponse}
236+
}
237+
238+
func (p *${XMethod}Result) GetSuccess() *${XResponse} {
239+
if !p.IsSetSuccess() {
240+
return ${XMethod}Result_Success_DEFAULT
241+
}
242+
return p.Success
243+
}
244+
```
245+
246+
The above generated code can be seen in kitex_gen directory.
247+
Therefore, there are three solutions for the business side to obtain the real req and resp:
248+
1. If you can determine which method is being called and the type of req used, you can directly obtain the specific Args type through type assertion, and then obtain the real req through the GetReq method.
249+
2. For thrift generated code, by asserting `GetFirstArgument` or `GetResult` , obtain `interface{}`, and then do type assertion to the real req or resp (Note: Since the returned `interface{}` contains a type, judging `interface{}` nil cannot intercept the case where req/resp itself is a null pointer, so we need to judge whether the asserted req/resp is a null pointer again);
250+
3. Obtain the real request/response body through reflection method, refer to the code:
251+
252+
```golang
253+
var ExampleMW endpoint.Middleware = func(next endpoint.Endpoint) endpoint.Endpoint {
254+
return func(ctx context.Context, request, response interface{}) error {
255+
reqV := reflect.ValueOf(request).MethodByName("GetReq").Call(nil)[0]
256+
log.Infof(ctx, "request: %T", reqV.Interface())
257+
err := next(ctx, request, response)
258+
respV := reflect.ValueOf(response).MethodByName("GetSuccess").Call(nil)[0]
259+
log.Infof(ctx, "response: %T", respV.Interface())
260+
return err
261+
}
262+
}
263+
```
87264

88-
If RPCInfo is used in a custom middleware, please pay attention to that RPCInfo will be recycled after the rpc is finished. If you start a goroutine in the middleware to modify RPCInfo, there will have some problems.

0 commit comments

Comments
 (0)