From 97a5ff616b70e88beae411d4e561763d480d65bf Mon Sep 17 00:00:00 2001 From: Daniel Liu Date: Mon, 4 Nov 2024 12:31:15 +0800 Subject: [PATCH] rpc: Make HTTP server timeout values configurable (#17240) --- cmd/XDC/main.go | 2 ++ cmd/XDC/usage.go | 2 ++ cmd/utils/flags.go | 22 +++++++++++++++++++--- node/api.go | 2 +- node/config.go | 9 +++++---- node/defaults.go | 13 ++++++------- node/node.go | 6 +++--- rpc/endpoints.go | 5 ++--- rpc/http.go | 30 ++++++++++++++++++++++++------ 9 files changed, 64 insertions(+), 27 deletions(-) diff --git a/cmd/XDC/main.go b/cmd/XDC/main.go index 864e5e1cd0..dcaad38cd9 100644 --- a/cmd/XDC/main.go +++ b/cmd/XDC/main.go @@ -147,7 +147,9 @@ var ( utils.RPCGlobalGasCapFlag, utils.RPCListenAddrFlag, utils.RPCPortFlag, + utils.RPCHttpReadTimeoutFlag, utils.RPCHttpWriteTimeoutFlag, + utils.RPCHttpIdleTimeoutFlag, utils.RPCApiFlag, utils.WSEnabledFlag, utils.WSListenAddrFlag, diff --git a/cmd/XDC/usage.go b/cmd/XDC/usage.go index b724155162..39a7c43673 100644 --- a/cmd/XDC/usage.go +++ b/cmd/XDC/usage.go @@ -147,7 +147,9 @@ var AppHelpFlagGroups = []flagGroup{ utils.RPCGlobalGasCapFlag, utils.RPCListenAddrFlag, utils.RPCPortFlag, + utils.RPCHttpReadTimeoutFlag, utils.RPCHttpWriteTimeoutFlag, + utils.RPCHttpIdleTimeoutFlag, utils.RPCApiFlag, utils.WSEnabledFlag, utils.WSListenAddrFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 5d661fa0e7..8041c2f337 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -435,10 +435,20 @@ var ( Usage: "HTTP-RPC server listening port", Value: node.DefaultHTTPPort, } + RPCHttpReadTimeoutFlag = cli.DurationFlag{ + Name: "rpcreadtimeout", + Usage: "HTTP-RPC server read timeout", + Value: rpc.DefaultHTTPTimeouts.ReadTimeout, + } RPCHttpWriteTimeoutFlag = cli.DurationFlag{ Name: "rpcwritetimeout", - Usage: "HTTP-RPC server write timeout (default = 10s)", - Value: node.DefaultHTTPWriteTimeOut, + Usage: "HTTP-RPC server write timeout", + Value: rpc.DefaultHTTPTimeouts.WriteTimeout, + } + RPCHttpIdleTimeoutFlag = cli.DurationFlag{ + Name: "rpcidletimeout", + Usage: "HTTP-RPC server idle timeout", + Value: rpc.DefaultHTTPTimeouts.IdleTimeout, } RPCCORSDomainFlag = cli.StringFlag{ Name: "rpccorsdomain", @@ -779,8 +789,14 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) { if ctx.GlobalIsSet(RPCPortFlag.Name) { cfg.HTTPPort = ctx.GlobalInt(RPCPortFlag.Name) } + if ctx.GlobalIsSet(RPCHttpReadTimeoutFlag.Name) { + cfg.HTTPTimeouts.ReadTimeout = ctx.GlobalDuration(RPCHttpReadTimeoutFlag.Name) + } if ctx.GlobalIsSet(RPCHttpWriteTimeoutFlag.Name) { - cfg.HTTPWriteTimeout = ctx.GlobalDuration(RPCHttpWriteTimeoutFlag.Name) + cfg.HTTPTimeouts.WriteTimeout = ctx.GlobalDuration(RPCHttpWriteTimeoutFlag.Name) + } + if ctx.GlobalIsSet(RPCHttpIdleTimeoutFlag.Name) { + cfg.HTTPTimeouts.IdleTimeout = ctx.GlobalDuration(RPCHttpIdleTimeoutFlag.Name) } if ctx.GlobalIsSet(RPCCORSDomainFlag.Name) { cfg.HTTPCors = splitAndTrim(ctx.GlobalString(RPCCORSDomainFlag.Name)) diff --git a/node/api.go b/node/api.go index 8a36115244..4db65d762f 100644 --- a/node/api.go +++ b/node/api.go @@ -158,7 +158,7 @@ func (api *PrivateAdminAPI) StartRPC(host *string, port *int, cors *string, apis } } - if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedVHosts); err != nil { + if err := api.node.startHTTP(fmt.Sprintf("%s:%d", *host, *port), api.node.rpcAPIs, modules, allowedOrigins, allowedVHosts, api.node.config.HTTPTimeouts); err != nil { return false, err } return true, nil diff --git a/node/config.go b/node/config.go index 848d563e9f..1984120f9f 100644 --- a/node/config.go +++ b/node/config.go @@ -23,7 +23,6 @@ import ( "path/filepath" "runtime" "strings" - "time" "github.com/XinFinOrg/XDPoSChain/accounts" "github.com/XinFinOrg/XDPoSChain/accounts/keystore" @@ -33,6 +32,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p" "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/rpc" ) const ( @@ -100,9 +100,6 @@ type Config struct { // for ephemeral nodes). HTTPPort int `toml:",omitempty"` - // HTTPWriteTimeout is the write timeout for the HTTP RPC server. - HTTPWriteTimeout time.Duration `toml:",omitempty"` - // HTTPCors is the Cross-Origin Resource Sharing header to send to requesting // clients. Please be aware that CORS is a browser enforced security, it's fully // useless for custom HTTP clients. @@ -122,6 +119,10 @@ type Config struct { // exposed. HTTPModules []string `toml:",omitempty"` + // HTTPTimeouts allows for customization of the timeout values used by the HTTP RPC + // interface. + HTTPTimeouts rpc.HTTPTimeouts + // WSHost is the host interface on which to start the websocket RPC server. If // this field is empty, no websocket API endpoint will be started. WSHost string `toml:",omitempty"` diff --git a/node/defaults.go b/node/defaults.go index f9f4f8f268..ff93489295 100644 --- a/node/defaults.go +++ b/node/defaults.go @@ -21,27 +21,26 @@ import ( "os/user" "path/filepath" "runtime" - "time" "github.com/XinFinOrg/XDPoSChain/p2p" "github.com/XinFinOrg/XDPoSChain/p2p/nat" + "github.com/XinFinOrg/XDPoSChain/rpc" ) const ( - DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server - DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server - DefaultHTTPWriteTimeOut = 10 * time.Second // Default write timeout for the HTTP RPC server - DefaultWSHost = "localhost" // Default host interface for the websocket RPC server - DefaultWSPort = 8546 // Default TCP port for the websocket RPC server + DefaultHTTPHost = "localhost" // Default host interface for the HTTP RPC server + DefaultHTTPPort = 8545 // Default TCP port for the HTTP RPC server + DefaultWSHost = "localhost" // Default host interface for the websocket RPC server + DefaultWSPort = 8546 // Default TCP port for the websocket RPC server ) // DefaultConfig contains reasonable default settings. var DefaultConfig = Config{ DataDir: DefaultDataDir(), HTTPPort: DefaultHTTPPort, - HTTPWriteTimeout: DefaultHTTPWriteTimeOut, HTTPModules: []string{"net", "web3"}, HTTPVirtualHosts: []string{"localhost"}, + HTTPTimeouts: rpc.DefaultHTTPTimeouts, WSPort: DefaultWSPort, WSModules: []string{"net", "web3"}, P2P: p2p.Config{ diff --git a/node/node.go b/node/node.go index c1092f33f7..fb1d9b4a99 100644 --- a/node/node.go +++ b/node/node.go @@ -269,7 +269,7 @@ func (n *Node) startRPC(services map[reflect.Type]Service) error { n.stopInProc() return err } - if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHosts); err != nil { + if err := n.startHTTP(n.httpEndpoint, apis, n.config.HTTPModules, n.config.HTTPCors, n.config.HTTPVirtualHosts, n.config.HTTPTimeouts); err != nil { n.stopIPC() n.stopInProc() return err @@ -348,12 +348,12 @@ func (n *Node) stopIPC() { } // startHTTP initializes and starts the HTTP RPC endpoint. -func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string, vhosts []string) error { +func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors []string, vhosts []string, timeouts rpc.HTTPTimeouts) error { // Short circuit if the HTTP endpoint isn't being exposed if endpoint == "" { return nil } - listener, handler, err := rpc.StartHTTPEndpoint(endpoint, apis, modules, cors, vhosts, n.config.HTTPWriteTimeout) + listener, handler, err := rpc.StartHTTPEndpoint(endpoint, apis, modules, cors, vhosts, timeouts) if err != nil { return err } diff --git a/rpc/endpoints.go b/rpc/endpoints.go index e5bd7d9648..21802c9542 100644 --- a/rpc/endpoints.go +++ b/rpc/endpoints.go @@ -19,13 +19,12 @@ package rpc import ( "net" "strings" - "time" "github.com/XinFinOrg/XDPoSChain/log" ) // StartHTTPEndpoint starts the HTTP RPC endpoint, configured with cors/vhosts/modules -func StartHTTPEndpoint(endpoint string, apis []API, modules []string, cors []string, vhosts []string, timeout time.Duration) (net.Listener, *Server, error) { +func StartHTTPEndpoint(endpoint string, apis []API, modules []string, cors []string, vhosts []string, timeouts HTTPTimeouts) (net.Listener, *Server, error) { // Generate the whitelist based on the allowed modules whitelist := make(map[string]bool) for _, module := range modules { @@ -49,7 +48,7 @@ func StartHTTPEndpoint(endpoint string, apis []API, modules []string, cors []str if listener, err = net.Listen("tcp", endpoint); err != nil { return nil, nil, err } - go NewHTTPServer(cors, vhosts, handler, timeout).Serve(listener) + go NewHTTPServer(cors, vhosts, timeouts, handler).Serve(listener) return listener, handler, err } diff --git a/rpc/http.go b/rpc/http.go index 6f267aa9d6..2416d66753 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -240,17 +240,35 @@ func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil } // NewHTTPServer creates a new HTTP RPC server around an API provider. // // Deprecated: Server implements http.Handler -func NewHTTPServer(cors []string, vhosts []string, srv *Server, writeTimeout time.Duration) *http.Server { +func NewHTTPServer(cors []string, vhosts []string, timeouts HTTPTimeouts, srv *Server) *http.Server { // Wrap the CORS-handler within a host-handler handler := newCorsHandler(srv, cors) handler = newVHostHandler(vhosts, handler) - handler = http.TimeoutHandler(handler, writeTimeout, `{"error":"http server timeout"}`) - log.Info("NewHTTPServer", "writeTimeout", writeTimeout) + + // Make sure timeout values are meaningful + if timeouts.ReadTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP read timeout", "provided", timeouts.ReadTimeout, "updated", DefaultHTTPTimeouts.ReadTimeout) + timeouts.ReadTimeout = DefaultHTTPTimeouts.ReadTimeout + } + if timeouts.WriteTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP write timeout", "provided", timeouts.WriteTimeout, "updated", DefaultHTTPTimeouts.WriteTimeout) + timeouts.WriteTimeout = DefaultHTTPTimeouts.WriteTimeout + } + if timeouts.IdleTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP idle timeout", "provided", timeouts.IdleTimeout, "updated", DefaultHTTPTimeouts.IdleTimeout) + timeouts.IdleTimeout = DefaultHTTPTimeouts.IdleTimeout + } + + // PR #469: return http code 503 and error message to client when timeout + handler = http.TimeoutHandler(handler, timeouts.WriteTimeout, `{"error":"http server timeout"}`) + log.Info("NewHTTPServer", "writeTimeout", timeouts.WriteTimeout) + + // Bundle and start the HTTP server return &http.Server{ Handler: handler, - ReadTimeout: 5 * time.Second, - WriteTimeout: writeTimeout + time.Second, - IdleTimeout: 120 * time.Second, + ReadTimeout: timeouts.ReadTimeout, + WriteTimeout: timeouts.WriteTimeout + time.Second, + IdleTimeout: timeouts.IdleTimeout, } }