forked from cerc-io/plugeth
graphql, node, rpc: improve HTTP write timeout handling (#25457)
Here we add special handling for sending an error response when the write timeout of the HTTP server is just about to expire. This is surprisingly difficult to get right, since is must be ensured that all output is fully flushed in time, which needs support from multiple levels of the RPC handler stack: The timeout response can't use chunked transfer-encoding because there is no way to write the final terminating chunk. net/http writes it when the topmost handler returns, but the timeout will already be over by the time that happens. We decided to disable chunked encoding by setting content-length explicitly. Gzip compression must also be disabled for timeout responses because we don't know the true content-length before compressing all output, i.e. compression would reintroduce chunked transfer-encoding.
This commit is contained in:
@@ -321,10 +321,11 @@ func TestGraphQLTransactionLogs(t *testing.T) {
|
||||
|
||||
func createNode(t *testing.T) *node.Node {
|
||||
stack, err := node.New(&node.Config{
|
||||
HTTPHost: "127.0.0.1",
|
||||
HTTPPort: 0,
|
||||
WSHost: "127.0.0.1",
|
||||
WSPort: 0,
|
||||
HTTPHost: "127.0.0.1",
|
||||
HTTPPort: 0,
|
||||
WSHost: "127.0.0.1",
|
||||
WSPort: 0,
|
||||
HTTPTimeouts: node.DefaultConfig.HTTPTimeouts,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("could not create node: %v", err)
|
||||
|
||||
+54
-11
@@ -20,12 +20,16 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/eth/filters"
|
||||
"github.com/ethereum/go-ethereum/internal/ethapi"
|
||||
"github.com/ethereum/go-ethereum/node"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
gqlErrors "github.com/graph-gophers/graphql-go/errors"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
@@ -43,21 +47,60 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
var (
|
||||
ctx = r.Context()
|
||||
responded sync.Once
|
||||
timer *time.Timer
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
response := h.Schema.Exec(ctx, params.Query, params.OperationName, params.Variables)
|
||||
responseJSON, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(response.Errors) > 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
if timeout, ok := rpc.ContextRequestTimeout(ctx); ok {
|
||||
timer = time.AfterFunc(timeout, func() {
|
||||
responded.Do(func() {
|
||||
// Cancel request handling.
|
||||
cancel()
|
||||
|
||||
// Create the timeout response.
|
||||
response := &graphql.Response{
|
||||
Errors: []*gqlErrors.QueryError{{Message: "request timed out"}},
|
||||
}
|
||||
responseJSON, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Setting this disables gzip compression in package node.
|
||||
w.Header().Set("transfer-encoding", "identity")
|
||||
|
||||
// Flush the response. Since we are writing close to the response timeout,
|
||||
// chunked transfer encoding must be disabled by setting content-length.
|
||||
w.Header().Set("content-type", "application/json")
|
||||
w.Header().Set("content-length", strconv.Itoa(len(responseJSON)))
|
||||
w.Write(responseJSON)
|
||||
if flush, ok := w.(http.Flusher); ok {
|
||||
flush.Flush()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(responseJSON)
|
||||
response := h.Schema.Exec(ctx, params.Query, params.OperationName, params.Variables)
|
||||
timer.Stop()
|
||||
responded.Do(func() {
|
||||
responseJSON, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(response.Errors) > 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
// New constructs a new GraphQL service instance.
|
||||
|
||||
Reference in New Issue
Block a user