| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- // Package jsonrpc2 provides a minimal JSON-RPC 2.0 HTTP client.
- //
- // The client is designed to work with the jsonrpc2 server implementation.
- // It supports only single JSON-RPC 2.0 requests and notifications.
- // Batch requests are explicitly unsupported.
- //
- // All request lifetimes are controlled by context.Context.
- //
- // Author: NiuJiuRu
- // Email: niujiuru@qq.com
- //
- // See: https://www.jsonrpc.org/specification
- package jsonrpc2
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "math"
- "net/http"
- "net/url"
- "sync/atomic"
- )
- // ClientOption configures an RPCClient.
- type ClientOption func(*RPCClient)
- // WithHTTPClient sets a custom http.Client.
- func WithHTTPClient(hc *http.Client) ClientOption {
- return func(c *RPCClient) {
- if hc != nil {
- c.http = hc
- }
- }
- }
- // SetHeader sets a custom HTTP header for all requests.
- func SetHeader(key, value string) ClientOption {
- return func(c *RPCClient) {
- if key == "" {
- return
- }
- if c.headers == nil {
- c.headers = make(http.Header)
- }
- c.headers.Set(key, value)
- }
- }
- // RPCClient is a minimal JSON-RPC 2.0 HTTP client.
- type RPCClient struct {
- baseURL string
- http *http.Client
- headers http.Header
- seq atomic.Int64
- }
- // NewRPCClient creates a new JSON-RPC 2.0 client.
- //
- // The client is minimal, safe for concurrent use, and designed to work
- // with standard JSON-RPC 2.0 HTTP servers.
- //
- // Example:
- //
- // // Create a client with default settings.
- // client := jsonrpc2.NewRPCClient("http://localhost:8080/rpc")
- //
- // // Create a client with a custom HTTP client and headers.
- // hc := &http.Client{
- // Timeout: 5 * time.Second,
- // }
- //
- // client := jsonrpc2.NewRPCClient(
- // "http://localhost:8080/rpc",
- // jsonrpc2.WithHTTPClient(hc),
- // jsonrpc2.SetHeader("Authorization", "Bearer token"),
- // jsonrpc2.SetHeader("X-Request-ID", "req-001"),
- // jsonrpc2.SetHeader("User-Agent", "jsonrpc2-client/1.0"),
- // )
- //
- // ctx := context.Background()
- // resp, err := client.Call(ctx, "add", []int{1, 2})
- //
- // Options are applied in order.
- // If no http.Client is provided, http.DefaultClient is used.
- func NewRPCClient(baseURL string, opts ...ClientOption) (*RPCClient, error) {
- if baseURL == "" {
- return nil, fmt.Errorf("baseURL cannot be empty")
- }
- _, err := url.Parse(baseURL)
- if err != nil {
- return nil, fmt.Errorf("invalid baseURL: %v", baseURL)
- }
- c := &RPCClient{
- baseURL: baseURL,
- http: http.DefaultClient,
- headers: http.Header{},
- }
- for _, opt := range opts {
- opt(c)
- }
- return c, nil
- }
- // Call invokes a JSON-RPC method and returns the response.
- func (c *RPCClient) Call(ctx context.Context, method string, params any) (*Response, error) {
- id := c.nextID()
- req, err := BuildRequest(method, params, id)
- if err != nil {
- return nil, fmt.Errorf("build request: %w", err)
- }
- return c.do(ctx, req)
- }
- // Notify sends a notification (no response expected).
- func (c *RPCClient) Notify(ctx context.Context, method string, params any) error {
- req, err := BuildNotification(method, params)
- if err != nil {
- return fmt.Errorf("build notification: %w", err)
- }
- _, err = c.do(ctx, req)
- return err
- }
- func (c *RPCClient) nextID() int64 {
- for {
- id := c.seq.Add(1)
- if id > 0 && id <= math.MaxInt32 {
- return id
- }
- if c.seq.CompareAndSwap(id, 1) {
- return 1
- }
- }
- }
- func (c *RPCClient) do(ctx context.Context, req *Request) (*Response, error) {
- body, err := json.Marshal(req)
- if err != nil {
- return nil, fmt.Errorf("marshal request: %w", err)
- }
- httpReq, err := http.NewRequestWithContext(
- ctx,
- http.MethodPost,
- c.baseURL,
- bytes.NewReader(body),
- )
- if err != nil {
- return nil, fmt.Errorf("create request: %w", err)
- }
- httpReq.Header = c.headers.Clone()
- httpReq.Header.Set("Content-Type", "application/json")
- httpReq.Header.Set("Accept", "application/json")
- resp, err := c.http.Do(httpReq)
- if err != nil {
- return nil, fmt.Errorf("http request: %w", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
- }
- if req.IsNotification() {
- _, _ = io.Copy(io.Discard, resp.Body)
- return nil, nil
- }
- data, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("read response: %w", err)
- }
- var rpcResp Response
- if err := json.Unmarshal(data, &rpcResp); err != nil {
- return nil, fmt.Errorf("unmarshal response: %w", err)
- }
- return &rpcResp, nil
- }
|