client.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. // Package jsonrpc2 provides a minimal JSON-RPC 2.0 HTTP client.
  2. //
  3. // The client is designed to work with the jsonrpc2 server implementation.
  4. // It supports only single JSON-RPC 2.0 requests and notifications.
  5. // Batch requests are explicitly unsupported.
  6. //
  7. // All request lifetimes are controlled by context.Context.
  8. //
  9. // Author: NiuJiuRu
  10. // Email: niujiuru@qq.com
  11. //
  12. // See: https://www.jsonrpc.org/specification
  13. package jsonrpc2
  14. import (
  15. "bytes"
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "math"
  21. "net/http"
  22. "net/url"
  23. "sync/atomic"
  24. )
  25. // ClientOption configures an RPCClient.
  26. type ClientOption func(*RPCClient)
  27. // WithHTTPClient sets a custom http.Client.
  28. func WithHTTPClient(hc *http.Client) ClientOption {
  29. return func(c *RPCClient) {
  30. if hc != nil {
  31. c.http = hc
  32. }
  33. }
  34. }
  35. // SetHeader sets a custom HTTP header for all requests.
  36. func SetHeader(key, value string) ClientOption {
  37. return func(c *RPCClient) {
  38. if key == "" {
  39. return
  40. }
  41. if c.headers == nil {
  42. c.headers = make(http.Header)
  43. }
  44. c.headers.Set(key, value)
  45. }
  46. }
  47. // RPCClient is a minimal JSON-RPC 2.0 HTTP client.
  48. type RPCClient struct {
  49. baseURL string
  50. http *http.Client
  51. headers http.Header
  52. seq atomic.Int64
  53. }
  54. // NewRPCClient creates a new JSON-RPC 2.0 client.
  55. //
  56. // The client is minimal, safe for concurrent use, and designed to work
  57. // with standard JSON-RPC 2.0 HTTP servers.
  58. //
  59. // Example:
  60. //
  61. // // Create a client with default settings.
  62. // client := jsonrpc2.NewRPCClient("http://localhost:8080/rpc")
  63. //
  64. // // Create a client with a custom HTTP client and headers.
  65. // hc := &http.Client{
  66. // Timeout: 5 * time.Second,
  67. // }
  68. //
  69. // client := jsonrpc2.NewRPCClient(
  70. // "http://localhost:8080/rpc",
  71. // jsonrpc2.WithHTTPClient(hc),
  72. // jsonrpc2.SetHeader("Authorization", "Bearer token"),
  73. // jsonrpc2.SetHeader("X-Request-ID", "req-001"),
  74. // jsonrpc2.SetHeader("User-Agent", "jsonrpc2-client/1.0"),
  75. // )
  76. //
  77. // ctx := context.Background()
  78. // resp, err := client.Call(ctx, "add", []int{1, 2})
  79. //
  80. // Options are applied in order.
  81. // If no http.Client is provided, http.DefaultClient is used.
  82. func NewRPCClient(baseURL string, opts ...ClientOption) (*RPCClient, error) {
  83. if baseURL == "" {
  84. return nil, fmt.Errorf("baseURL cannot be empty")
  85. }
  86. _, err := url.Parse(baseURL)
  87. if err != nil {
  88. return nil, fmt.Errorf("invalid baseURL: %v", baseURL)
  89. }
  90. c := &RPCClient{
  91. baseURL: baseURL,
  92. http: http.DefaultClient,
  93. headers: http.Header{},
  94. }
  95. for _, opt := range opts {
  96. opt(c)
  97. }
  98. return c, nil
  99. }
  100. // Call invokes a JSON-RPC method and returns the response.
  101. func (c *RPCClient) Call(ctx context.Context, method string, params any) (*Response, error) {
  102. id := c.nextID()
  103. req, err := BuildRequest(method, params, id)
  104. if err != nil {
  105. return nil, fmt.Errorf("build request: %w", err)
  106. }
  107. return c.do(ctx, req)
  108. }
  109. // Notify sends a notification (no response expected).
  110. func (c *RPCClient) Notify(ctx context.Context, method string, params any) error {
  111. req, err := BuildNotification(method, params)
  112. if err != nil {
  113. return fmt.Errorf("build notification: %w", err)
  114. }
  115. _, err = c.do(ctx, req)
  116. return err
  117. }
  118. func (c *RPCClient) nextID() int64 {
  119. for {
  120. id := c.seq.Add(1)
  121. if id > 0 && id <= math.MaxInt32 {
  122. return id
  123. }
  124. if c.seq.CompareAndSwap(id, 1) {
  125. return 1
  126. }
  127. }
  128. }
  129. func (c *RPCClient) do(ctx context.Context, req *Request) (*Response, error) {
  130. body, err := json.Marshal(req)
  131. if err != nil {
  132. return nil, fmt.Errorf("marshal request: %w", err)
  133. }
  134. httpReq, err := http.NewRequestWithContext(
  135. ctx,
  136. http.MethodPost,
  137. c.baseURL,
  138. bytes.NewReader(body),
  139. )
  140. if err != nil {
  141. return nil, fmt.Errorf("create request: %w", err)
  142. }
  143. httpReq.Header = c.headers.Clone()
  144. httpReq.Header.Set("Content-Type", "application/json")
  145. httpReq.Header.Set("Accept", "application/json")
  146. resp, err := c.http.Do(httpReq)
  147. if err != nil {
  148. return nil, fmt.Errorf("http request: %w", err)
  149. }
  150. defer resp.Body.Close()
  151. if resp.StatusCode != http.StatusOK {
  152. return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
  153. }
  154. if req.IsNotification() {
  155. _, _ = io.Copy(io.Discard, resp.Body)
  156. return nil, nil
  157. }
  158. data, err := io.ReadAll(resp.Body)
  159. if err != nil {
  160. return nil, fmt.Errorf("read response: %w", err)
  161. }
  162. var rpcResp Response
  163. if err := json.Unmarshal(data, &rpcResp); err != nil {
  164. return nil, fmt.Errorf("unmarshal response: %w", err)
  165. }
  166. return &rpcResp, nil
  167. }