14 "github.com/bytom/bytom/errors"
15 "github.com/bytom/bytom/net/http/httperror"
16 "github.com/bytom/bytom/net/http/reqid"
19 // Bytom-specific header fields
21 HeaderBlockchainID = "Blockchain-ID"
22 HeaderCoreID = "Bytom-Core-ID"
23 HeaderTimeout = "RPC-Timeout"
26 // ErrWrongNetwork is returned when a peer's blockchain ID differs from
27 // the RPC client's blockchain ID.
28 var ErrWrongNetwork = errors.New("connected to a peer on a different network")
30 // A Client is a Bytom RPC client. It performs RPCs over HTTP using JSON
31 // request and responses. A Client must be configured with a secret token
32 // to authenticate with other Cores on the network.
41 // If set, Client is used for outgoing requests.
42 // TODO(kr): make this required (crash on nil)
46 func (c Client) userAgent() string {
47 return fmt.Sprintf("Bytom; process=%s; buildtag=%s; blockchainID=%s",
48 c.Username, c.BuildTag, c.BlockchainID)
51 // ErrStatusCode is an error returned when an rpc fails with a non-200
53 type ErrStatusCode struct {
56 ErrorData *httperror.Response
59 func (e ErrStatusCode) Error() string {
60 return fmt.Sprintf("Request to `%s` responded with %d %s",
61 e.URL, e.StatusCode, http.StatusText(e.StatusCode))
64 // Call calls a remote procedure on another node, specified by the path.
65 func (c *Client) Call(ctx context.Context, path string, request, response interface{}) error {
66 r, err := c.CallRaw(ctx, path, request)
72 decoder := json.NewDecoder(r)
74 err = errors.Wrap(decoder.Decode(response))
79 // CallRaw calls a remote procedure on another node, specified by the path. It
80 // returns a io.ReadCloser of the raw response body.
81 func (c *Client) CallRaw(ctx context.Context, path string, request interface{}) (io.ReadCloser, error) {
82 u, err := url.Parse(c.BaseURL)
84 return nil, errors.Wrap(err)
88 var bodyReader io.Reader
90 var jsonBody bytes.Buffer
91 if err := json.NewEncoder(&jsonBody).Encode(request); err != nil {
92 return nil, errors.Wrap(err)
94 bodyReader = &jsonBody
97 req, err := http.NewRequest("POST", u.String(), bodyReader)
99 return nil, errors.Wrap(err)
102 if c.AccessToken != "" {
103 var username, password string
104 toks := strings.SplitN(c.AccessToken, ":", 2)
111 req.SetBasicAuth(username, password)
114 // Propagate our request ID so that we can trace a request across nodes.
115 req.Header.Add("Request-ID", reqid.FromContext(ctx))
116 req.Header.Set("Content-Type", "application/json")
117 req.Header.Set("User-Agent", c.userAgent())
118 req.Header.Set(HeaderBlockchainID, c.BlockchainID)
119 req.Header.Set(HeaderCoreID, c.CoreID)
121 // Propagate our deadline if we have one.
122 deadline, ok := ctx.Deadline()
124 req.Header.Set(HeaderTimeout, deadline.Sub(time.Now()).String())
129 client = http.DefaultClient
131 resp, err := client.Do(req.WithContext(ctx))
132 if err != nil && ctx.Err() != nil { // check if it timed out
133 return nil, errors.Wrap(ctx.Err())
134 } else if err != nil {
135 return nil, errors.Wrap(err)
138 if id := resp.Header.Get(HeaderBlockchainID); c.BlockchainID != "" && id != "" && c.BlockchainID != id {
140 return nil, errors.Wrap(ErrWrongNetwork)
143 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
144 defer resp.Body.Close()
146 resErr := ErrStatusCode{
147 URL: cleanedURLString(u),
148 StatusCode: resp.StatusCode,
151 // Attach formatted error message, if available
152 var errData httperror.Response
153 err := json.NewDecoder(resp.Body).Decode(&errData)
154 if err == nil && errData.ChainCode != "" {
155 resErr.ErrorData = &errData
161 return resp.Body, nil
164 func cleanedURLString(u *url.URL) string {