From a41fdd75f9b7506b4cf9c0135c808469e448c380 Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Thu, 25 Apr 2024 22:13:15 +0800
Subject: [PATCH 1/8] fix pointer dereference partially
---
rpc/getAccountInfo.go | 2 +-
rpc/getAccountInfoWithRpcContext.go | 6 +++++-
rpc/getMultipleAccounts.go | 2 +-
rpc/getSignatureStatuses.go | 2 +-
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/rpc/getAccountInfo.go b/rpc/getAccountInfo.go
index fdd0384bf..9d922fcda 100644
--- a/rpc/getAccountInfo.go
+++ b/rpc/getAccountInfo.go
@@ -99,7 +99,7 @@ func (cl *Client) GetAccountInfoWithOpts(
if err != nil {
return nil, err
}
- if out.Value == nil {
+ if out == nil || out.Value == nil {
return nil, ErrNotFound
}
return out, nil
diff --git a/rpc/getAccountInfoWithRpcContext.go b/rpc/getAccountInfoWithRpcContext.go
index 6a06ffe8d..ab0628252 100644
--- a/rpc/getAccountInfoWithRpcContext.go
+++ b/rpc/getAccountInfoWithRpcContext.go
@@ -32,5 +32,9 @@ func (cl *Client) GetAccountInfoWithRpcContext(
if err != nil {
return nil, nil, err
}
- return out.Value, &out.RPCContext, nil
+ if out == nil {
+ return nil, nil, nil
+ } else {
+ return out.Value, &out.RPCContext, nil
+ }
}
diff --git a/rpc/getMultipleAccounts.go b/rpc/getMultipleAccounts.go
index af316cf98..cd88d1451 100644
--- a/rpc/getMultipleAccounts.go
+++ b/rpc/getMultipleAccounts.go
@@ -74,7 +74,7 @@ func (cl *Client) GetMultipleAccountsWithOpts(
if err != nil {
return nil, err
}
- if out.Value == nil {
+ if out == nil || out.Value == nil {
return nil, ErrNotFound
}
return
diff --git a/rpc/getSignatureStatuses.go b/rpc/getSignatureStatuses.go
index 48cc8c18f..44e2cad01 100644
--- a/rpc/getSignatureStatuses.go
+++ b/rpc/getSignatureStatuses.go
@@ -43,7 +43,7 @@ func (cl *Client) GetSignatureStatuses(
if err != nil {
return nil, err
}
- if out.Value == nil {
+ if out == nil || out.Value == nil {
// Unknown transaction
return nil, ErrNotFound
}
From ba94bf038da86582af59ea9161bd803240f2e289 Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Wed, 23 Jul 2025 16:23:03 +0800
Subject: [PATCH 2/8] http error
---
rpc/jsonrpc/jsonrpc.go | 147 ++++++++++++++++++++++++++++-------------
1 file changed, 102 insertions(+), 45 deletions(-)
diff --git a/rpc/jsonrpc/jsonrpc.go b/rpc/jsonrpc/jsonrpc.go
index 6a48ec95f..40b8a8bd3 100644
--- a/rpc/jsonrpc/jsonrpc.go
+++ b/rpc/jsonrpc/jsonrpc.go
@@ -235,9 +235,22 @@ func (e *RPCError) Error() string {
// and the body could not be parsed to a valid RPCResponse object that holds a RPCError.
//
// Otherwise a RPCResponse object is returned with a RPCError field that is not nil.
+//type HTTPError struct {
+// Code int
+// err error
+//}
+
type HTTPError struct {
- Code int
- err error
+ StatusCode int
+ Status string
+ Body []byte
+}
+
+func (err HTTPError) Error() string {
+ if len(err.Body) == 0 {
+ return err.Status
+ }
+ return fmt.Sprintf("%v: %s", err.Status, err.Body)
}
// HTTPClient is an abstraction for a HTTP client
@@ -246,17 +259,18 @@ type HTTPClient interface {
CloseIdleConnections()
}
-func NewHTTPError(code int, err error) *HTTPError {
+func NewHTTPError(code int, message string, body []byte) *HTTPError {
return &HTTPError{
- Code: code,
- err: err,
+ StatusCode: code,
+ Status: message,
+ Body: body,
}
}
// Error function is provided to be used as error object.
-func (e *HTTPError) Error() string {
- return e.err.Error()
-}
+//func (e *HTTPError) Error() string {
+// return e.err.Error()
+//}
type rpcClient struct {
endpoint string
@@ -358,6 +372,12 @@ func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {
return rpcClient
}
+func (client *rpcClient) SetCustomHeaders(headers map[string]string) {
+ for k, v := range headers {
+ client.customHeaders[k] = v
+ }
+}
+
func (client *rpcClient) Call(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error) {
request := &RPCRequest{
Method: method,
@@ -499,26 +519,28 @@ func (client *rpcClient) doCall(
err := decoder.Decode(&rpcResponse)
// parsing error
if err != nil {
+ return err
// if we have some http error, return it
- if httpResponse.StatusCode >= 400 {
- return &HTTPError{
- Code: httpResponse.StatusCode,
- err: fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %w", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode, err),
- }
- }
- return fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %w", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode, err)
+ //if httpResponse.StatusCode >= 400 {
+ // return &HTTPError{
+ // Code: httpResponse.StatusCode,
+ // err: fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %w", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode, err),
+ // }
+ //}
+ //return fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %w", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode, err)
}
// response body empty
if rpcResponse == nil {
+ return fmt.Errorf("rpc response is empty")
// if we have some http error, return it
- if httpResponse.StatusCode >= 400 {
- return &HTTPError{
- Code: httpResponse.StatusCode,
- err: fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode),
- }
- }
- return fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode)
+ //if httpResponse.StatusCode >= 400 {
+ // return &HTTPError{
+ // Code: httpResponse.StatusCode,
+ // err: fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode),
+ // }
+ //}
+ //return fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode)
}
return nil
},
@@ -558,16 +580,34 @@ func (client *rpcClient) doCallWithCallbackOnHTTPResponse(
}
httpRequest, err := client.newRequest(ctx, RPCRequest)
if err != nil {
- if httpRequest != nil {
- return fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.String(), err)
- }
- return fmt.Errorf("rpc call %v(): %w", RPCRequest.Method, err)
+ return err
+ //if httpRequest != nil {
+ // return fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.String(), err)
+ //}
+ //return fmt.Errorf("rpc call %v(): %w", RPCRequest.Method, err)
}
httpResponse, err := client.httpClient.Do(httpRequest)
if err != nil {
- return fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.String(), err)
+ return err
}
- defer httpResponse.Body.Close()
+ if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
+ var buf bytes.Buffer
+ var body []byte
+ if _, err := buf.ReadFrom(httpResponse.Body); err == nil {
+ body = buf.Bytes()
+ }
+ httpResponse.Body.Close()
+ return HTTPError{
+ Status: httpResponse.Status,
+ StatusCode: httpResponse.StatusCode,
+ Body: body,
+ }
+ }
+
+ //if err != nil {
+ // return fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.String(), err)
+ //}
+ //defer httpResponse.Body.Close()
return callback(httpRequest, httpResponse)
}
@@ -581,10 +621,23 @@ func (client *rpcClient) doBatchCall(ctx context.Context, rpcRequest []*RPCReque
return nil, fmt.Errorf("rpc batch call: %w", err)
}
httpResponse, err := client.httpClient.Do(httpRequest)
- if err != nil {
- return nil, fmt.Errorf("rpc batch call on %v: %w", httpRequest.URL.String(), err)
+ if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
+ var buf bytes.Buffer
+ var body []byte
+ if _, err := buf.ReadFrom(httpResponse.Body); err == nil {
+ body = buf.Bytes()
+ }
+ httpResponse.Body.Close()
+ return nil, HTTPError{
+ Status: httpResponse.Status,
+ StatusCode: httpResponse.StatusCode,
+ Body: body,
+ }
}
- defer httpResponse.Body.Close()
+ //if err != nil {
+ // return nil, fmt.Errorf("rpc batch call on %v: %w", httpRequest.URL.String(), err)
+ //}
+ //defer httpResponse.Body.Close()
var rpcResponse RPCResponses
decoder := json.NewDecoder(httpResponse.Body)
@@ -593,26 +646,30 @@ func (client *rpcClient) doBatchCall(ctx context.Context, rpcRequest []*RPCReque
err = decoder.Decode(&rpcResponse)
// parsing error
if err != nil {
+ return nil, err
// if we have some http error, return it
- if httpResponse.StatusCode >= 400 {
- return nil, &HTTPError{
- Code: httpResponse.StatusCode,
- err: fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %w", httpRequest.URL.String(), httpResponse.StatusCode, err),
- }
- }
- return nil, fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %w", httpRequest.URL.String(), httpResponse.StatusCode, err)
+ //if httpResponse.StatusCode >= 400 {
+ // return nil, &HTTPError{
+ // StatusCode: httpResponse.StatusCode,
+ // Status: httpResponse.Status,
+ // Body:
+ // //err: fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %w", httpRequest.URL.String(), httpResponse.StatusCode, err),
+ // }
+ //}
+ //return nil, fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %w", httpRequest.URL.String(), httpResponse.StatusCode, err)
}
// response body empty
if rpcResponse == nil || len(rpcResponse) == 0 {
+ return nil, fmt.Errorf("JSON-RPC response has no result")
// if we have some http error, return it
- if httpResponse.StatusCode >= 400 {
- return nil, &HTTPError{
- Code: httpResponse.StatusCode,
- err: fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode),
- }
- }
- return nil, fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode)
+ //if httpResponse.StatusCode >= 400 {
+ // return nil, &HTTPError{
+ // Code: httpResponse.StatusCode,
+ // err: fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode),
+ // }
+ //}
+ //return nil, fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode)
}
return rpcResponse, nil
From 588d29718748a75ee81b6afd81cbc6211b146bde Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Wed, 23 Jul 2025 23:43:59 +0800
Subject: [PATCH 3/8] new client with dynamic header
---
rpc/client.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/rpc/client.go b/rpc/client.go
index 722e0e58c..8f72565a4 100644
--- a/rpc/client.go
+++ b/rpc/client.go
@@ -23,6 +23,7 @@ import (
"io"
"net"
"net/http"
+ "sync/atomic"
"time"
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
@@ -37,6 +38,7 @@ var (
type Client struct {
rpcURL string
rpcClient JSONRPCClient
+ headerFn atomic.Value
}
type JSONRPCClient interface {
@@ -149,3 +151,58 @@ func NewBoolean(b bool) *bool {
func NewTransactionVersion(v uint64) *uint64 {
return &v
}
+
+type HeaderRoundTripper struct {
+ Base http.RoundTripper
+ Headers func() map[string]string
+}
+
+func (h *HeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ for k, v := range h.Headers() {
+ req.Header.Set(k, v)
+ }
+ return h.Base.RoundTrip(req)
+}
+
+func newHTTPWithHeaderFn(headerFn func() map[string]string) *http.Client {
+ baseTransport := newHTTPTransport()
+
+ transport := &HeaderRoundTripper{
+ Base: gzhttp.Transport(baseTransport),
+ Headers: headerFn,
+ }
+
+ return &http.Client{
+ Timeout: defaultTimeout,
+ Transport: transport,
+ }
+}
+
+func NewWithDynamicHeaders(rpcEndpoint string) *Client {
+ client := &Client{}
+
+ // default header function is nil
+ // it will be set later by the user.
+ client.headerFn.Store(func() map[string]string {
+ return map[string]string{}
+ })
+
+ httpClient := newHTTPWithHeaderFn(func() map[string]string {
+ if v := client.headerFn.Load(); v != nil {
+ if f, ok := v.(func() map[string]string); ok {
+ return f()
+ }
+ }
+ return nil
+ })
+
+ rpcClient := jsonrpc.NewClientWithOpts(rpcEndpoint, &jsonrpc.RPCClientOpts{
+ HTTPClient: httpClient,
+ })
+ client.rpcClient = rpcClient
+ return client
+}
+
+func (cl *Client) SetHeaderFunc(f func() map[string]string) {
+ cl.headerFn.Store(f)
+}
From b4b1e582bb9528859d4784c0dea6737008154313 Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Thu, 24 Jul 2025 15:48:25 +0800
Subject: [PATCH 4/8] add header
---
rpc/client.go | 86 ++++++++++++++----------------------------
rpc/client_test.go | 2 +
rpc/jsonrpc/jsonrpc.go | 38 +++++++++++++++++--
3 files changed, 66 insertions(+), 60 deletions(-)
diff --git a/rpc/client.go b/rpc/client.go
index 8f72565a4..4496327ae 100644
--- a/rpc/client.go
+++ b/rpc/client.go
@@ -23,7 +23,6 @@ import (
"io"
"net"
"net/http"
- "sync/atomic"
"time"
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
@@ -38,7 +37,6 @@ var (
type Client struct {
rpcURL string
rpcClient JSONRPCClient
- headerFn atomic.Value
}
type JSONRPCClient interface {
@@ -69,6 +67,35 @@ func NewWithHeaders(rpcEndpoint string, headers map[string]string) *Client {
return NewWithCustomRPCClient(rpcClient)
}
+// SetHeader dynamically adds or modifies a header for the current client.
+func (cl *Client) SetHeader(key, value string) {
+
+ if rc, ok := cl.rpcClient.(interface {
+ SetHeader(key, value string)
+ }); ok {
+ rc.SetHeader(key, value)
+ }
+}
+
+// RemoveHeader dynamically removes a header for the current client.
+func (cl *Client) RemoveHeader(key string) {
+ if rc, ok := cl.rpcClient.(interface {
+ RemoveHeader(key string)
+ }); ok {
+ rc.RemoveHeader(key)
+ }
+}
+
+// GetHeaders retrieves all headers currently set for the client.
+func (cl *Client) GetHeaders() map[string]string {
+ if rc, ok := cl.rpcClient.(interface {
+ GetHeaders() map[string]string
+ }); ok {
+ return rc.GetHeaders()
+ }
+ return nil
+}
+
// Close closes the client.
func (cl *Client) Close() error {
if cl.rpcClient == nil {
@@ -151,58 +178,3 @@ func NewBoolean(b bool) *bool {
func NewTransactionVersion(v uint64) *uint64 {
return &v
}
-
-type HeaderRoundTripper struct {
- Base http.RoundTripper
- Headers func() map[string]string
-}
-
-func (h *HeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
- for k, v := range h.Headers() {
- req.Header.Set(k, v)
- }
- return h.Base.RoundTrip(req)
-}
-
-func newHTTPWithHeaderFn(headerFn func() map[string]string) *http.Client {
- baseTransport := newHTTPTransport()
-
- transport := &HeaderRoundTripper{
- Base: gzhttp.Transport(baseTransport),
- Headers: headerFn,
- }
-
- return &http.Client{
- Timeout: defaultTimeout,
- Transport: transport,
- }
-}
-
-func NewWithDynamicHeaders(rpcEndpoint string) *Client {
- client := &Client{}
-
- // default header function is nil
- // it will be set later by the user.
- client.headerFn.Store(func() map[string]string {
- return map[string]string{}
- })
-
- httpClient := newHTTPWithHeaderFn(func() map[string]string {
- if v := client.headerFn.Load(); v != nil {
- if f, ok := v.(func() map[string]string); ok {
- return f()
- }
- }
- return nil
- })
-
- rpcClient := jsonrpc.NewClientWithOpts(rpcEndpoint, &jsonrpc.RPCClientOpts{
- HTTPClient: httpClient,
- })
- client.rpcClient = rpcClient
- return client
-}
-
-func (cl *Client) SetHeaderFunc(f func() map[string]string) {
- cl.headerFn.Store(f)
-}
diff --git a/rpc/client_test.go b/rpc/client_test.go
index ff65c26eb..a8ad62977 100644
--- a/rpc/client_test.go
+++ b/rpc/client_test.go
@@ -24,6 +24,8 @@ import (
stdjson "encoding/json"
"fmt"
"math/big"
+ "net/http"
+ "sync"
"testing"
"github.com/AlekSi/pointer"
diff --git a/rpc/jsonrpc/jsonrpc.go b/rpc/jsonrpc/jsonrpc.go
index 40b8a8bd3..6252690f3 100644
--- a/rpc/jsonrpc/jsonrpc.go
+++ b/rpc/jsonrpc/jsonrpc.go
@@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"reflect"
+ "sync"
"sync/atomic"
"github.com/davecgh/go-spew/spew"
@@ -276,6 +277,7 @@ type rpcClient struct {
endpoint string
httpClient HTTPClient
customHeaders map[string]string
+ headersMutex sync.RWMutex
}
// RPCClientOpts can be provided to NewClientWithOpts() to change configuration of RPCClient.
@@ -349,10 +351,16 @@ func NewClient(endpoint string) RPCClient {
//
// opts: RPCClientOpts provide custom configuration
func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {
+ if endpoint == "" {
+ panic("endpoint must not be empty")
+ }
+
+ // create the rpcClient and set default values
rpcClient := &rpcClient{
endpoint: endpoint,
httpClient: &http.Client{},
customHeaders: make(map[string]string),
+ headersMutex: sync.RWMutex{},
}
if opts == nil {
@@ -372,10 +380,32 @@ func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {
return rpcClient
}
-func (client *rpcClient) SetCustomHeaders(headers map[string]string) {
- for k, v := range headers {
- client.customHeaders[k] = v
+// SetHeader can be used to dynamically add or modify a header for the current client.
+func (r *rpcClient) SetHeader(key, value string) {
+ r.headersMutex.Lock()
+ defer r.headersMutex.Unlock()
+ if r.customHeaders == nil {
+ r.customHeaders = make(map[string]string)
+ }
+ r.customHeaders[key] = value
+}
+
+// RemoveHeader can be used to dynamically remove a header for the current client.
+func (r *rpcClient) RemoveHeader(key string) {
+ r.headersMutex.Lock()
+ defer r.headersMutex.Unlock()
+ delete(r.customHeaders, key)
+}
+
+// GetHeaders returns a copy of the current headers set for the client.
+func (r *rpcClient) GetHeaders() map[string]string {
+ r.headersMutex.RLock()
+ defer r.headersMutex.RUnlock()
+ copied := make(map[string]string, len(r.customHeaders))
+ for k, v := range r.customHeaders {
+ copied[k] = v
}
+ return copied
}
func (client *rpcClient) Call(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error) {
@@ -497,6 +527,8 @@ func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http
request.Header.Set("Accept", "application/json")
// set default headers first, so that even content type and accept can be overwritten
+ client.headersMutex.RLock()
+ defer client.headersMutex.RUnlock()
for k, v := range client.customHeaders {
request.Header.Set(k, v)
}
From 355a9bef0c7b0ddc2110716b0fcecfc7bfaf428a Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Thu, 24 Jul 2025 15:56:58 +0800
Subject: [PATCH 5/8] testcase
---
rpc/client_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 75 insertions(+), 1 deletion(-)
diff --git a/rpc/client_test.go b/rpc/client_test.go
index a8ad62977..4500bc076 100644
--- a/rpc/client_test.go
+++ b/rpc/client_test.go
@@ -25,7 +25,8 @@ import (
"fmt"
"math/big"
"net/http"
- "sync"
+ "net/http/httptest"
+ "sync/atomic"
"testing"
"github.com/AlekSi/pointer"
@@ -3125,3 +3126,76 @@ func TestClient_GetRecentPrioritizationFees(t *testing.T) {
assert.Equal(t, expected, got, "both deserialized values must be equal")
}
+
+// mockServer will check if the request header contains the specified key/value
+func mockServer(t *testing.T, wantKey, wantValue string, called *atomic.Bool) *httptest.Server {
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if got := r.Header.Get(wantKey); got != wantValue {
+ t.Errorf("header %q: got %q, want %q", wantKey, got, wantValue)
+ }
+ called.Store(true)
+ _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":{},"id":1}`))
+ })
+ return httptest.NewServer(handler)
+}
+
+func TestClient_SetHeader_AddAndModify(t *testing.T) {
+ var called atomic.Bool
+ server := mockServer(t, "Authorization", "Bearer testtoken", &called)
+ defer server.Close()
+
+ cli := New(server.URL)
+
+ cli.SetHeader("Authorization", "Bearer testtoken")
+ // send a request to trigger the server
+ _ = cli.RPCCallForInto(context.Background(), &struct{}{}, "getVersion", nil)
+ if !called.Load() {
+ t.Fatalf("server was not called")
+ }
+
+ // modify header
+ called.Store(false)
+ cli.SetHeader("Authorization", "Bearer newtoken")
+ s2 := mockServer(t, "Authorization", "Bearer newtoken", &called)
+ defer s2.Close()
+ cli = New(s2.URL)
+ cli.SetHeader("Authorization", "Bearer newtoken")
+ _ = cli.RPCCallForInto(context.Background(), &struct{}{}, "getVersion", nil)
+ if !called.Load() {
+ t.Fatalf("server was not called after modifying header")
+ }
+}
+
+func TestClient_SetHeader_RemoveHeader(t *testing.T) {
+ var called atomic.Bool
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if got := r.Header.Get("X-Remove-Me"); got != "" {
+ t.Errorf("header X-Remove-Me should be empty after removal, got %q", got)
+ }
+ called.Store(true)
+ _, _ = w.Write([]byte(`{"jsonrpc":"2.0","result":{},"id":1}`))
+ }))
+ defer server.Close()
+
+ cli := New(server.URL)
+ cli.SetHeader("X-Remove-Me", "to-be-removed")
+ cli.RemoveHeader("X-Remove-Me")
+ _ = cli.RPCCallForInto(context.Background(), &struct{}{}, "getVersion", nil)
+ if !called.Load() {
+ t.Fatalf("server was not called for RemoveHeader test")
+ }
+}
+
+func TestClient_GetHeaders(t *testing.T) {
+ cli := New("http://localhost")
+ cli.SetHeader("A", "1")
+ cli.SetHeader("B", "2")
+ cli.RemoveHeader("A")
+ headers := cli.GetHeaders()
+ if _, ok := headers["A"]; ok {
+ t.Errorf("header A should be removed")
+ }
+ if v, ok := headers["B"]; !ok || v != "2" {
+ t.Errorf("header B missing or incorrect")
+ }
+}
From 9d7ac1936b629fb33f2339bbaf1cac1ffaa6e865 Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Mon, 28 Jul 2025 17:52:59 +0800
Subject: [PATCH 6/8] new context with headers
---
rpc/jsonrpc/context_headers.go | 56 ++++++++++++++++++++++++++++++++++
rpc/jsonrpc/jsonrpc.go | 35 ++-------------------
2 files changed, 58 insertions(+), 33 deletions(-)
create mode 100644 rpc/jsonrpc/context_headers.go
diff --git a/rpc/jsonrpc/context_headers.go b/rpc/jsonrpc/context_headers.go
new file mode 100644
index 000000000..810eeb473
--- /dev/null
+++ b/rpc/jsonrpc/context_headers.go
@@ -0,0 +1,56 @@
+// Copyright 2022 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package jsonrpc
+
+import (
+ "context"
+ "net/http"
+)
+
+type mdHeaderKey struct{}
+
+// NewContextWithHeaders wraps the given context, adding HTTP headers. These headers will
+// be applied by Client when making a request using the returned context.
+func NewContextWithHeaders(ctx context.Context, h http.Header) context.Context {
+ if len(h) == 0 {
+ // This check ensures the header map set in context will never be nil.
+ return ctx
+ }
+
+ var ctxh http.Header
+ prev, ok := ctx.Value(mdHeaderKey{}).(http.Header)
+ if ok {
+ ctxh = setHeaders(prev.Clone(), h)
+ } else {
+ ctxh = h.Clone()
+ }
+ return context.WithValue(ctx, mdHeaderKey{}, ctxh)
+}
+
+// headersFromContext is used to extract http.Header from context.
+func headersFromContext(ctx context.Context) http.Header {
+ source, _ := ctx.Value(mdHeaderKey{}).(http.Header)
+ return source
+}
+
+// setHeaders sets all headers from src in dst.
+func setHeaders(dst http.Header, src http.Header) http.Header {
+ for key, values := range src {
+ dst[http.CanonicalHeaderKey(key)] = values
+ }
+ return dst
+}
diff --git a/rpc/jsonrpc/jsonrpc.go b/rpc/jsonrpc/jsonrpc.go
index 6252690f3..104242a3e 100644
--- a/rpc/jsonrpc/jsonrpc.go
+++ b/rpc/jsonrpc/jsonrpc.go
@@ -9,7 +9,6 @@ import (
"fmt"
"net/http"
"reflect"
- "sync"
"sync/atomic"
"github.com/davecgh/go-spew/spew"
@@ -277,7 +276,6 @@ type rpcClient struct {
endpoint string
httpClient HTTPClient
customHeaders map[string]string
- headersMutex sync.RWMutex
}
// RPCClientOpts can be provided to NewClientWithOpts() to change configuration of RPCClient.
@@ -360,7 +358,6 @@ func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {
endpoint: endpoint,
httpClient: &http.Client{},
customHeaders: make(map[string]string),
- headersMutex: sync.RWMutex{},
}
if opts == nil {
@@ -380,34 +377,6 @@ func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {
return rpcClient
}
-// SetHeader can be used to dynamically add or modify a header for the current client.
-func (r *rpcClient) SetHeader(key, value string) {
- r.headersMutex.Lock()
- defer r.headersMutex.Unlock()
- if r.customHeaders == nil {
- r.customHeaders = make(map[string]string)
- }
- r.customHeaders[key] = value
-}
-
-// RemoveHeader can be used to dynamically remove a header for the current client.
-func (r *rpcClient) RemoveHeader(key string) {
- r.headersMutex.Lock()
- defer r.headersMutex.Unlock()
- delete(r.customHeaders, key)
-}
-
-// GetHeaders returns a copy of the current headers set for the client.
-func (r *rpcClient) GetHeaders() map[string]string {
- r.headersMutex.RLock()
- defer r.headersMutex.RUnlock()
- copied := make(map[string]string, len(r.customHeaders))
- for k, v := range r.customHeaders {
- copied[k] = v
- }
- return copied
-}
-
func (client *rpcClient) Call(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error) {
request := &RPCRequest{
Method: method,
@@ -527,11 +496,11 @@ func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http
request.Header.Set("Accept", "application/json")
// set default headers first, so that even content type and accept can be overwritten
- client.headersMutex.RLock()
- defer client.headersMutex.RUnlock()
for k, v := range client.customHeaders {
request.Header.Set(k, v)
}
+ // set headers from context
+ setHeaders(request.Header, headersFromContext(ctx))
return request, nil
}
From 7325c006c9a83bbc0efb2c3daf7ca711e4f423b0 Mon Sep 17 00:00:00 2001
From: 0xcary <0xcary.web3@gmail.com>
Date: Mon, 28 Jul 2025 17:56:39 +0800
Subject: [PATCH 7/8] remove functions with header
---
rpc/client.go | 29 -----------------------------
1 file changed, 29 deletions(-)
diff --git a/rpc/client.go b/rpc/client.go
index 4496327ae..722e0e58c 100644
--- a/rpc/client.go
+++ b/rpc/client.go
@@ -67,35 +67,6 @@ func NewWithHeaders(rpcEndpoint string, headers map[string]string) *Client {
return NewWithCustomRPCClient(rpcClient)
}
-// SetHeader dynamically adds or modifies a header for the current client.
-func (cl *Client) SetHeader(key, value string) {
-
- if rc, ok := cl.rpcClient.(interface {
- SetHeader(key, value string)
- }); ok {
- rc.SetHeader(key, value)
- }
-}
-
-// RemoveHeader dynamically removes a header for the current client.
-func (cl *Client) RemoveHeader(key string) {
- if rc, ok := cl.rpcClient.(interface {
- RemoveHeader(key string)
- }); ok {
- rc.RemoveHeader(key)
- }
-}
-
-// GetHeaders retrieves all headers currently set for the client.
-func (cl *Client) GetHeaders() map[string]string {
- if rc, ok := cl.rpcClient.(interface {
- GetHeaders() map[string]string
- }); ok {
- return rc.GetHeaders()
- }
- return nil
-}
-
// Close closes the client.
func (cl *Client) Close() error {
if cl.rpcClient == nil {
From ec0b01da912d70abd1211682a4e7ff943a472652 Mon Sep 17 00:00:00 2001
From: cary
Date: Wed, 29 Oct 2025 15:46:07 +0800
Subject: [PATCH 8/8] fix handle error
---
rpc/jsonrpc/jsonrpc.go | 27 +++++++++++----------------
1 file changed, 11 insertions(+), 16 deletions(-)
diff --git a/rpc/jsonrpc/jsonrpc.go b/rpc/jsonrpc/jsonrpc.go
index 104242a3e..6fe7be67d 100644
--- a/rpc/jsonrpc/jsonrpc.go
+++ b/rpc/jsonrpc/jsonrpc.go
@@ -7,6 +7,7 @@ import (
stdjson "encoding/json"
"errors"
"fmt"
+ "io"
"net/http"
"reflect"
"sync/atomic"
@@ -582,22 +583,21 @@ func (client *rpcClient) doCallWithCallbackOnHTTPResponse(
httpRequest, err := client.newRequest(ctx, RPCRequest)
if err != nil {
return err
- //if httpRequest != nil {
- // return fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.String(), err)
- //}
- //return fmt.Errorf("rpc call %v(): %w", RPCRequest.Method, err)
}
httpResponse, err := client.httpClient.Do(httpRequest)
if err != nil {
return err
}
+ defer httpResponse.Body.Close()
+
+ // allow callback to process first (regardless of status code)
+ if err := callback(httpRequest, httpResponse); err != nil {
+ return err
+ }
+
+ // if HTTP status code is not 2xx, return HTTPError additionally
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
- var buf bytes.Buffer
- var body []byte
- if _, err := buf.ReadFrom(httpResponse.Body); err == nil {
- body = buf.Bytes()
- }
- httpResponse.Body.Close()
+ body, _ := io.ReadAll(io.LimitReader(httpResponse.Body, 4<<10)) // 最多4KB
return HTTPError{
Status: httpResponse.Status,
StatusCode: httpResponse.StatusCode,
@@ -605,12 +605,7 @@ func (client *rpcClient) doCallWithCallbackOnHTTPResponse(
}
}
- //if err != nil {
- // return fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.String(), err)
- //}
- //defer httpResponse.Body.Close()
-
- return callback(httpRequest, httpResponse)
+ return nil
}
func (client *rpcClient) doBatchCall(ctx context.Context, rpcRequest []*RPCRequest) ([]*RPCResponse, error) {