mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-10 05:50:19 +00:00
We should not set cache-control headers on RPC responses. HTTP caching interacts poorly with resources that are expected to change frequently, or whose rate of change is unpredictable. More subtly, all calls to the POST endpoint use the same URL, which means a cacheable response from one call may actually "hide" an uncacheable response from a subsequent one. This is less of a problem for the GET endpoints, but that means the behaviour of RPCs varies depending on which HTTP method your client happens to use. Websocket requests were already marked statically uncacheable, adding yet a third combination. To address this: - Stop setting cache-control headers. - Update the tests that were checking for those headers. - Remove the flags to request cache-control. Apart from affecting the HTTP response headers, this change does not modify the behaviour of any of the RPC methods.
315 lines
8.8 KiB
Go
315 lines
8.8 KiB
Go
// Commons for HTTP handling
|
|
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/net/netutil"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
|
)
|
|
|
|
// Config is a RPC server configuration.
|
|
type Config struct {
|
|
// see netutil.LimitListener
|
|
MaxOpenConnections int
|
|
// mirrors http.Server#ReadTimeout
|
|
ReadTimeout time.Duration
|
|
// mirrors http.Server#WriteTimeout
|
|
WriteTimeout time.Duration
|
|
// MaxBodyBytes controls the maximum number of bytes the
|
|
// server will read parsing the request body.
|
|
MaxBodyBytes int64
|
|
// mirrors http.Server#MaxHeaderBytes
|
|
MaxHeaderBytes int
|
|
}
|
|
|
|
// DefaultConfig returns a default configuration.
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
MaxOpenConnections: 0, // unlimited
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxBodyBytes: int64(1000000), // 1MB
|
|
MaxHeaderBytes: 1 << 20, // same as the net/http default
|
|
}
|
|
}
|
|
|
|
// Serve creates a http.Server and calls Serve with the given listener. It
|
|
// wraps handler with RecoverAndLogHandler and a handler, which limits the max
|
|
// body size to config.MaxBodyBytes.
|
|
//
|
|
// NOTE: This function blocks - you may want to call it in a go-routine.
|
|
func Serve(
|
|
ctx context.Context,
|
|
listener net.Listener,
|
|
handler http.Handler,
|
|
logger log.Logger,
|
|
config *Config,
|
|
) error {
|
|
logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr()))
|
|
s := &http.Server{
|
|
Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger),
|
|
ReadTimeout: config.ReadTimeout,
|
|
WriteTimeout: config.WriteTimeout,
|
|
MaxHeaderBytes: config.MaxHeaderBytes,
|
|
}
|
|
sig := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
sctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
_ = s.Shutdown(sctx)
|
|
case <-sig:
|
|
}
|
|
}()
|
|
|
|
if err := s.Serve(listener); err != nil {
|
|
logger.Info("RPC HTTP server stopped", "err", err)
|
|
close(sig)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Serve creates a http.Server and calls ServeTLS with the given listener,
|
|
// certFile and keyFile. It wraps handler with RecoverAndLogHandler and a
|
|
// handler, which limits the max body size to config.MaxBodyBytes.
|
|
//
|
|
// NOTE: This function blocks - you may want to call it in a go-routine.
|
|
func ServeTLS(
|
|
ctx context.Context,
|
|
listener net.Listener,
|
|
handler http.Handler,
|
|
certFile, keyFile string,
|
|
logger log.Logger,
|
|
config *Config,
|
|
) error {
|
|
logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)",
|
|
listener.Addr(), certFile, keyFile))
|
|
s := &http.Server{
|
|
Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger),
|
|
ReadTimeout: config.ReadTimeout,
|
|
WriteTimeout: config.WriteTimeout,
|
|
MaxHeaderBytes: config.MaxHeaderBytes,
|
|
}
|
|
sig := make(chan struct{})
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
sctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
_ = s.Shutdown(sctx)
|
|
case <-sig:
|
|
}
|
|
}()
|
|
|
|
if err := s.ServeTLS(listener, certFile, keyFile); err != nil {
|
|
logger.Error("RPC HTTPS server stopped", "err", err)
|
|
close(sig)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WriteRPCResponseHTTPError marshals res as JSON (with indent) and writes it
|
|
// to w.
|
|
//
|
|
// Maps JSON RPC error codes to HTTP Status codes as follows:
|
|
//
|
|
// HTTP Status code message
|
|
// 500 -32700 Parse error.
|
|
// 400 -32600 Invalid Request.
|
|
// 404 -32601 Method not found.
|
|
// 500 -32602 Invalid params.
|
|
// 500 -32603 Internal error.
|
|
// 500 -32099..-32000 Server error.
|
|
//
|
|
// source: https://www.jsonrpc.org/historical/json-rpc-over-http.html
|
|
func WriteRPCResponseHTTPError(
|
|
w http.ResponseWriter,
|
|
res rpctypes.RPCResponse,
|
|
) error {
|
|
if res.Error == nil {
|
|
panic("tried to write http error response without RPC error")
|
|
}
|
|
|
|
jsonBytes, err := json.MarshalIndent(res, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("json marshal: %w", err)
|
|
}
|
|
|
|
var httpCode int
|
|
switch res.Error.Code {
|
|
case -32600:
|
|
httpCode = http.StatusBadRequest
|
|
case -32601:
|
|
httpCode = http.StatusNotFound
|
|
default:
|
|
httpCode = http.StatusInternalServerError
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(httpCode)
|
|
_, err = w.Write(jsonBytes)
|
|
return err
|
|
}
|
|
|
|
// WriteRPCResponseHTTP marshals res as JSON (with indent) and writes it to w.
|
|
// If the rpc response can be cached, add cache-control to the response header.
|
|
func WriteRPCResponseHTTP(w http.ResponseWriter, res ...rpctypes.RPCResponse) error {
|
|
var v interface{}
|
|
if len(res) == 1 {
|
|
v = res[0]
|
|
} else {
|
|
v = res
|
|
}
|
|
|
|
jsonBytes, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("json marshal: %w", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, err = w.Write(jsonBytes)
|
|
return err
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// RecoverAndLogHandler wraps an HTTP handler, adding error logging.
|
|
// If the inner function panics, the outer function recovers, logs, sends an
|
|
// HTTP 500 error response.
|
|
func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Wrap the ResponseWriter to remember the status
|
|
rww := &responseWriterWrapper{-1, w}
|
|
begin := time.Now()
|
|
|
|
rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix()))
|
|
|
|
defer func() {
|
|
// Handle any panics in the panic handler below. Does not use the logger, since we want
|
|
// to avoid any further panics. However, we try to return a 500, since it otherwise
|
|
// defaults to 200 and there is no other way to terminate the connection. If that
|
|
// should panic for whatever reason then the Go HTTP server will handle it and
|
|
// terminate the connection - panicing is the de-facto and only way to get the Go HTTP
|
|
// server to terminate the request and close the connection/stream:
|
|
// https://github.com/golang/go/issues/17790#issuecomment-258481416
|
|
if e := recover(); e != nil {
|
|
fmt.Fprintf(os.Stderr, "Panic during RPC panic recovery: %v\n%v\n", e, string(debug.Stack()))
|
|
w.WriteHeader(500)
|
|
}
|
|
}()
|
|
|
|
defer func() {
|
|
// Send a 500 error if a panic happens during a handler.
|
|
// Without this, Chrome & Firefox were retrying aborted ajax requests,
|
|
// at least to my localhost.
|
|
if e := recover(); e != nil {
|
|
|
|
// If RPCResponse
|
|
if res, ok := e.(rpctypes.RPCResponse); ok {
|
|
if wErr := WriteRPCResponseHTTP(rww, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
} else {
|
|
// Panics can contain anything, attempt to normalize it as an error.
|
|
var err error
|
|
switch e := e.(type) {
|
|
case error:
|
|
err = e
|
|
case string:
|
|
err = errors.New(e)
|
|
case fmt.Stringer:
|
|
err = errors.New(e.String())
|
|
default:
|
|
}
|
|
|
|
logger.Error("panic in RPC HTTP handler", "err", e, "stack", string(debug.Stack()))
|
|
|
|
res := rpctypes.RPCInternalError(rpctypes.JSONRPCIntID(-1), err)
|
|
if wErr := WriteRPCResponseHTTPError(rww, res); wErr != nil {
|
|
logger.Error("failed to write response", "res", res, "err", wErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, log.
|
|
durationMS := time.Since(begin).Nanoseconds() / 1000000
|
|
if rww.Status == -1 {
|
|
rww.Status = 200
|
|
}
|
|
logger.Debug("served RPC HTTP response",
|
|
"method", r.Method,
|
|
"url", r.URL,
|
|
"status", rww.Status,
|
|
"duration", durationMS,
|
|
"remoteAddr", r.RemoteAddr,
|
|
)
|
|
}()
|
|
|
|
handler.ServeHTTP(rww, r)
|
|
})
|
|
}
|
|
|
|
// Remember the status for logging
|
|
type responseWriterWrapper struct {
|
|
Status int
|
|
http.ResponseWriter
|
|
}
|
|
|
|
func (w *responseWriterWrapper) WriteHeader(status int) {
|
|
w.Status = status
|
|
w.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
// implements http.Hijacker
|
|
func (w *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
return w.ResponseWriter.(http.Hijacker).Hijack()
|
|
}
|
|
|
|
type maxBytesHandler struct {
|
|
h http.Handler
|
|
n int64
|
|
}
|
|
|
|
func (h maxBytesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, h.n)
|
|
h.h.ServeHTTP(w, r)
|
|
}
|
|
|
|
// Listen starts a new net.Listener on the given address.
|
|
// It returns an error if the address is invalid or the call to Listen() fails.
|
|
func Listen(addr string, maxOpenConnections int) (listener net.Listener, err error) {
|
|
parts := strings.SplitN(addr, "://", 2)
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf(
|
|
"invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)",
|
|
addr,
|
|
)
|
|
}
|
|
proto, addr := parts[0], parts[1]
|
|
listener, err = net.Listen(proto, addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to listen on %v: %v", addr, err)
|
|
}
|
|
if maxOpenConnections > 0 {
|
|
listener = netutil.LimitListener(listener, maxOpenConnections)
|
|
}
|
|
|
|
return listener, nil
|
|
}
|