diff --git a/avalanchego/api/server/server.go b/avalanchego/api/server/server.go index 0b3e1910c..ee84b6191 100644 --- a/avalanchego/api/server/server.go +++ b/avalanchego/api/server/server.go @@ -16,6 +16,7 @@ import ( "time" "github.com/NYTimes/gziphandler" + "golang.org/x/net/http2" "github.com/gorilla/handlers" @@ -29,7 +30,10 @@ import ( "github.com/flare-foundation/flare/utils/logging" ) -const baseURL = "/ext" +const ( + baseURL = "/ext" + maxConcurrentStreams = 64 +) var ( errUnknownLockOption = errors.New("invalid lock options") @@ -41,6 +45,13 @@ type RouteAdder interface { AddRoute(handler *common.HTTPHandler, lock *sync.RWMutex, base, endpoint string, loggingWriter io.Writer) error } +type HTTPConfig struct { + ReadTimeout time.Duration `json:"readTimeout"` + ReadHeaderTimeout time.Duration `json:"readHeaderTimeout"` + WriteTimeout time.Duration `json:"writeHeaderTimeout"` + IdleTimeout time.Duration `json:"idleTimeout"` +} + // Server maintains the HTTP router type Server struct { // log this server writes to @@ -59,12 +70,15 @@ type Server struct { router *router srv *http.Server + + httpConfig *HTTPConfig } // Initialize creates the API server at the provided host and port func (s *Server) Initialize( log logging.Logger, factory logging.Factory, + httpConfig *HTTPConfig, host string, port uint16, allowedOrigins []string, @@ -74,13 +88,12 @@ func (s *Server) Initialize( ) { s.log = log s.factory = factory + s.httpConfig = httpConfig s.listenHost = host s.listenPort = port s.shutdownTimeout = shutdownTimeout s.router = newRouter() - s.log.Info("API created with allowed origins: %v", allowedOrigins) - corsHandler := cors.New(cors.Options{ AllowedOrigins: allowedOrigins, AllowCredentials: true, @@ -97,6 +110,25 @@ func (s *Server) Initialize( for _, wrapper := range wrappers { s.handler = wrapper.WrapHandler(s.handler) } + +} + +func (s *Server) newHttpServer(listenAddress string) (*http.Server, error) { + httpServer := &http.Server{ + Addr: listenAddress, + ReadTimeout: s.httpConfig.ReadTimeout, + ReadHeaderTimeout: s.httpConfig.ReadHeaderTimeout, + WriteTimeout: s.httpConfig.WriteTimeout, + IdleTimeout: s.httpConfig.IdleTimeout, + Handler: s.handler, + } + err := http2.ConfigureServer(httpServer, &http2.Server{ + MaxConcurrentStreams: maxConcurrentStreams, + }) + if err != nil { + return nil, err + } + return httpServer, nil } // Dispatch starts the API server @@ -114,7 +146,10 @@ func (s *Server) Dispatch() error { s.log.Info("HTTP API server listening on \"%s:%d\"", s.listenHost, ipDesc.Port) } - s.srv = &http.Server{Handler: s.handler} + s.srv, err = s.newHttpServer("") + if err != nil { + return err + } return s.srv.Serve(listener) } @@ -142,7 +177,10 @@ func (s *Server) DispatchTLS(certBytes, keyBytes []byte) error { s.log.Info("HTTPS API server listening on \"%s:%d\"", s.listenHost, ipDesc.Port) } - s.srv = &http.Server{Addr: listenAddress, Handler: s.handler} + s.srv, err = s.newHttpServer(listenAddress) + if err != nil { + return err + } return s.srv.Serve(listener) } diff --git a/avalanchego/config/config.go b/avalanchego/config/config.go index 4c3614126..dabb9de58 100644 --- a/avalanchego/config/config.go +++ b/avalanchego/config/config.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/viper" + "github.com/flare-foundation/flare/api/server" "github.com/flare-foundation/flare/app/runner" "github.com/flare-foundation/flare/chains" "github.com/flare-foundation/flare/genesis" @@ -232,6 +233,12 @@ func getHTTPConfig(v *viper.Viper) (node.HTTPConfig, error) { } config := node.HTTPConfig{ + HTTPConfig: server.HTTPConfig{ + ReadTimeout: v.GetDuration(HTTPReadTimeoutKey), + ReadHeaderTimeout: v.GetDuration(HTTPReadHeaderTimeoutKey), + WriteTimeout: v.GetDuration(HTTPWriteTimeoutKey), + IdleTimeout: v.GetDuration(HTTPIdleTimeoutKey), + }, APIConfig: node.APIConfig{ APIIndexerConfig: node.APIIndexerConfig{ IndexAPIEnabled: v.GetBool(IndexEnabledKey), diff --git a/avalanchego/config/flags.go b/avalanchego/config/flags.go index c3c87b4dc..068e47333 100644 --- a/avalanchego/config/flags.go +++ b/avalanchego/config/flags.go @@ -192,6 +192,10 @@ func addNodeFlags(fs *flag.FlagSet) { fs.String(HTTPAllowedOrigins, "*", "Origins to allow on the HTTP port. Defaults to * which allows all origins. Example: https://*.avax.network https://*.avax-test.network") fs.Duration(HTTPShutdownWaitKey, 0, "Duration to wait after receiving SIGTERM or SIGINT before initiating shutdown. The /health endpoint will return unhealthy during this duration") fs.Duration(HTTPShutdownTimeoutKey, 10*time.Second, "Maximum duration to wait for existing connections to complete during node shutdown") + fs.Duration(HTTPReadTimeoutKey, 30*time.Second, "Maximum duration for reading the entire request, including the body. A zero or negative value means there will be no timeout") + fs.Duration(HTTPReadHeaderTimeoutKey, 30*time.Second, fmt.Sprintf("Maximum duration to read request headers. The connection's read deadline is reset after reading the headers. If %s is zero, the value of %s is used. If both are zero, there is no timeout.", HTTPReadHeaderTimeoutKey, HTTPReadTimeoutKey)) + fs.Duration(HTTPWriteTimeoutKey, 30*time.Second, "Maximum duration before timing out writes of the response. It is reset whenever a new request's header is read. A zero or negative value means there will be no timeout.") + fs.Duration(HTTPIdleTimeoutKey, 120*time.Second, fmt.Sprintf("Maximum duration to wait for the next request when keep-alives are enabled. If %s is zero, the value of %s is used. If both are zero, there is no timeout.", HTTPIdleTimeoutKey, HTTPReadTimeoutKey)) fs.Bool(APIAuthRequiredKey, false, "Require authorization token to call HTTP APIs") fs.String(APIAuthPasswordFileKey, "", fmt.Sprintf("Password file used to initially create/validate API authorization tokens. Ignored if %s is specified. Leading and trailing whitespace is removed from the password. Can be changed via API call", diff --git a/avalanchego/config/keys.go b/avalanchego/config/keys.go index e252cad05..fa889d7cb 100644 --- a/avalanchego/config/keys.go +++ b/avalanchego/config/keys.go @@ -51,6 +51,10 @@ const ( HTTPAllowedOrigins = "http-allowed-origins" HTTPShutdownTimeoutKey = "http-shutdown-timeout" HTTPShutdownWaitKey = "http-shutdown-wait" + HTTPReadTimeoutKey = "http-read-timeout" + HTTPReadHeaderTimeoutKey = "http-read-header-timeout" + HTTPWriteTimeoutKey = "http-write-timeout" + HTTPIdleTimeoutKey = "http-idle-timeout" APIAuthRequiredKey = "api-auth-required" APIAuthPasswordKey = "api-auth-password" APIAuthPasswordFileKey = "api-auth-password-file" diff --git a/avalanchego/node/config.go b/avalanchego/node/config.go index c6930ad37..25472c14e 100644 --- a/avalanchego/node/config.go +++ b/avalanchego/node/config.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "time" + "github.com/flare-foundation/flare/api/server" "github.com/flare-foundation/flare/chains" "github.com/flare-foundation/flare/genesis" "github.com/flare-foundation/flare/ids" @@ -40,6 +41,7 @@ type APIIndexerConfig struct { } type HTTPConfig struct { + server.HTTPConfig APIConfig `json:"apiConfig"` HTTPHost string `json:"httpHost"` HTTPPort uint16 `json:"httpPort"` diff --git a/avalanchego/node/node.go b/avalanchego/node/node.go index 4d4831695..e79841979 100644 --- a/avalanchego/node/node.go +++ b/avalanchego/node/node.go @@ -495,6 +495,7 @@ func (n *Node) initAPIServer() error { n.APIServer.Initialize( n.Log, n.LogFactory, + &n.Config.HTTPConfig.HTTPConfig, n.Config.HTTPHost, n.Config.HTTPPort, n.Config.APIAllowedOrigins, @@ -512,6 +513,7 @@ func (n *Node) initAPIServer() error { n.APIServer.Initialize( n.Log, n.LogFactory, + &n.Config.HTTPConfig.HTTPConfig, n.Config.HTTPHost, n.Config.HTTPPort, n.Config.APIAllowedOrigins,