Allow custom headers in validator client HTTP requests (#15884)

* Allow custom headers in validator client HTTP requests

* changelog <3

* improve flag description

* Bastin's review

* James' review

* add godoc for NodeConnectionOption
This commit is contained in:
Radosław Kapka
2025-10-22 15:47:35 +02:00
committed by GitHub
parent 3ecb5d0b67
commit 2f090c52d9
15 changed files with 160 additions and 9 deletions

View File

@@ -84,7 +84,7 @@ func (acm *CLIManager) prepareBeaconClients(ctx context.Context) (*iface.Validat
conn := validatorHelpers.NewNodeConnection(
grpcConn,
acm.beaconApiEndpoint,
acm.beaconApiTimeout,
validatorHelpers.WithBeaconApiTimeout(acm.beaconApiTimeout),
)
restHandler := beaconApi.NewBeaconApiRestHandler(

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
api "github.com/OffchainLabs/prysm/v6/api/client"
eventClient "github.com/OffchainLabs/prysm/v6/api/client/event"
grpcutil "github.com/OffchainLabs/prysm/v6/api/grpc"
"github.com/OffchainLabs/prysm/v6/async/event"
@@ -79,6 +80,7 @@ type Config struct {
BeaconNodeGRPCEndpoint string
BeaconNodeCert string
BeaconApiEndpoint string
BeaconApiHeaders map[string][]string
BeaconApiTimeout time.Duration
Graffiti string
GraffitiStruct *graffiti.Graffiti
@@ -142,7 +144,8 @@ func NewValidatorService(ctx context.Context, cfg *Config) (*ValidatorService, e
s.conn = validatorHelpers.NewNodeConnection(
grpcConn,
cfg.BeaconApiEndpoint,
cfg.BeaconApiTimeout,
validatorHelpers.WithBeaconApiHeaders(cfg.BeaconApiHeaders),
validatorHelpers.WithBeaconApiTimeout(cfg.BeaconApiTimeout),
)
return s, nil
@@ -185,8 +188,9 @@ func (v *ValidatorService) Start() {
return
}
headersTransport := api.NewCustomHeadersTransport(http.DefaultTransport, v.conn.GetBeaconApiHeaders())
restHandler := beaconApi.NewBeaconApiRestHandler(
http.Client{Timeout: v.conn.GetBeaconApiTimeout(), Transport: otelhttp.NewTransport(http.DefaultTransport)},
http.Client{Timeout: v.conn.GetBeaconApiTimeout(), Transport: otelhttp.NewTransport(headersTransport)},
hosts[0],
)

View File

@@ -10,16 +10,37 @@ import (
type NodeConnection interface {
GetGrpcClientConn() *grpc.ClientConn
GetBeaconApiUrl() string
GetBeaconApiHeaders() map[string][]string
setBeaconApiHeaders(map[string][]string)
GetBeaconApiTimeout() time.Duration
setBeaconApiTimeout(time.Duration)
dummy()
}
type nodeConnection struct {
grpcClientConn *grpc.ClientConn
beaconApiUrl string
beaconApiHeaders map[string][]string
beaconApiTimeout time.Duration
}
// NodeConnectionOption is a functional option for configuring the node connection.
type NodeConnectionOption func(nc NodeConnection)
// WithBeaconApiHeaders sets the HTTP headers that should be sent to the server along with each request.
func WithBeaconApiHeaders(headers map[string][]string) NodeConnectionOption {
return func(nc NodeConnection) {
nc.setBeaconApiHeaders(headers)
}
}
// WithBeaconApiTimeout sets the HTTP request timeout.
func WithBeaconApiTimeout(timeout time.Duration) NodeConnectionOption {
return func(nc NodeConnection) {
nc.setBeaconApiTimeout(timeout)
}
}
func (c *nodeConnection) GetGrpcClientConn() *grpc.ClientConn {
return c.grpcClientConn
}
@@ -28,16 +49,30 @@ func (c *nodeConnection) GetBeaconApiUrl() string {
return c.beaconApiUrl
}
func (c *nodeConnection) GetBeaconApiHeaders() map[string][]string {
return c.beaconApiHeaders
}
func (c *nodeConnection) setBeaconApiHeaders(headers map[string][]string) {
c.beaconApiHeaders = headers
}
func (c *nodeConnection) GetBeaconApiTimeout() time.Duration {
return c.beaconApiTimeout
}
func (c *nodeConnection) setBeaconApiTimeout(timeout time.Duration) {
c.beaconApiTimeout = timeout
}
func (*nodeConnection) dummy() {}
func NewNodeConnection(grpcConn *grpc.ClientConn, beaconApiUrl string, beaconApiTimeout time.Duration) NodeConnection {
func NewNodeConnection(grpcConn *grpc.ClientConn, beaconApiUrl string, opts ...NodeConnectionOption) NodeConnection {
conn := &nodeConnection{}
conn.grpcClientConn = grpcConn
conn.beaconApiUrl = beaconApiUrl
conn.beaconApiTimeout = beaconApiTimeout
for _, opt := range opts {
opt(conn)
}
return conn
}

View File

@@ -433,6 +433,7 @@ func (c *ValidatorClient) registerValidatorService(cliCtx *cli.Context) error {
BeaconNodeGRPCEndpoint: cliCtx.String(flags.BeaconRPCProviderFlag.Name),
BeaconNodeCert: cliCtx.String(flags.CertFlag.Name),
BeaconApiEndpoint: cliCtx.String(flags.BeaconRESTApiProviderFlag.Name),
BeaconApiHeaders: parseBeaconApiHeaders(cliCtx.String(flags.BeaconRESTApiHeaders.Name)),
BeaconApiTimeout: time.Second * 30,
Graffiti: g.ParseHexGraffiti(cliCtx.String(flags.GraffitiFlag.Name)),
GraffitiStruct: graffitiStruct,
@@ -552,6 +553,7 @@ func (c *ValidatorClient) registerRPCService(cliCtx *cli.Context) error {
GRPCHeaders: strings.Split(cliCtx.String(flags.GRPCHeadersFlag.Name), ","),
BeaconNodeGRPCEndpoint: cliCtx.String(flags.BeaconRPCProviderFlag.Name),
BeaconApiEndpoint: cliCtx.String(flags.BeaconRESTApiProviderFlag.Name),
BeaconAPIHeaders: parseBeaconApiHeaders(cliCtx.String(flags.BeaconRESTApiHeaders.Name)),
BeaconApiTimeout: time.Second * 30,
BeaconNodeCert: cliCtx.String(flags.CertFlag.Name),
DB: c.db,
@@ -636,3 +638,23 @@ func clearDB(ctx context.Context, dataDir string, force bool, isDatabaseMinimal
return nil
}
func parseBeaconApiHeaders(rawHeaders string) map[string][]string {
result := make(map[string][]string)
pairs := strings.Split(rawHeaders, ",")
for _, pair := range pairs {
key, value, found := strings.Cut(pair, "=")
if !found {
// Skip malformed pairs
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
// Skip malformed pairs
continue
}
result[key] = append(result[key], value)
}
return result
}

View File

@@ -308,3 +308,17 @@ func TestWeb3SignerConfig(t *testing.T) {
})
}
}
func Test_parseBeaconApiHeaders(t *testing.T) {
t.Run("ok", func(t *testing.T) {
h := parseBeaconApiHeaders("key1=value1,key1=value2,key2=value3")
assert.Equal(t, 2, len(h))
assert.DeepEqual(t, []string{"value1", "value2"}, h["key1"])
assert.DeepEqual(t, []string{"value3"}, h["key2"])
})
t.Run("ignores malformed", func(t *testing.T) {
h := parseBeaconApiHeaders("key1=value1,key2value2,key3=,=key4")
assert.Equal(t, 1, len(h))
assert.DeepEqual(t, []string{"value1"}, h["key1"])
})
}

View File

@@ -23,6 +23,7 @@ go_library(
],
deps = [
"//api:go_default_library",
"//api/client:go_default_library",
"//api/grpc:go_default_library",
"//api/pagination:go_default_library",
"//api/server:go_default_library",

View File

@@ -3,6 +3,7 @@ package rpc
import (
"net/http"
api "github.com/OffchainLabs/prysm/v6/api/client"
grpcutil "github.com/OffchainLabs/prysm/v6/api/grpc"
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v6/validator/client"
@@ -52,11 +53,13 @@ func (s *Server) registerBeaconClient() error {
conn := validatorHelpers.NewNodeConnection(
grpcConn,
s.beaconApiEndpoint,
s.beaconApiTimeout,
validatorHelpers.WithBeaconApiHeaders(s.beaconApiHeaders),
validatorHelpers.WithBeaconApiTimeout(s.beaconApiTimeout),
)
headersTransport := api.NewCustomHeadersTransport(http.DefaultTransport, conn.GetBeaconApiHeaders())
restHandler := beaconApi.NewBeaconApiRestHandler(
http.Client{Timeout: s.beaconApiTimeout, Transport: otelhttp.NewTransport(http.DefaultTransport)},
http.Client{Timeout: s.beaconApiTimeout, Transport: otelhttp.NewTransport(headersTransport)},
s.beaconApiEndpoint,
)

View File

@@ -34,6 +34,7 @@ type Config struct {
GRPCHeaders []string
BeaconNodeGRPCEndpoint string
BeaconApiEndpoint string
BeaconAPIHeaders map[string][]string
BeaconApiTimeout time.Duration
BeaconNodeCert string
DB db.Database
@@ -64,6 +65,7 @@ type Server struct {
authTokenPath string
beaconNodeCert string
beaconApiEndpoint string
beaconApiHeaders map[string][]string
beaconNodeEndpoint string
healthClient ethpb.HealthClient
nodeClient iface.NodeClient
@@ -103,6 +105,7 @@ func NewServer(ctx context.Context, cfg *Config) *Server {
wallet: cfg.Wallet,
beaconApiTimeout: cfg.BeaconApiTimeout,
beaconApiEndpoint: cfg.BeaconApiEndpoint,
beaconApiHeaders: cfg.BeaconAPIHeaders,
beaconNodeEndpoint: cfg.BeaconNodeGRPCEndpoint,
router: cfg.Router,
}