erigon-pulse/cl/beacon/beaconhttp/api.go
2023-12-15 02:27:27 +01:00

109 lines
3.0 KiB
Go

package beaconhttp
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/ledgerwatch/erigon-lib/types/ssz"
"github.com/ledgerwatch/erigon/cl/phase1/forkchoice/fork_graph"
"github.com/ledgerwatch/log/v3"
"golang.org/x/exp/slices"
)
var _ error = EndpointError{}
var _ error = (*EndpointError)(nil)
type EndpointError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func WrapEndpointError(err error) *EndpointError {
e := &EndpointError{}
if errors.As(err, e) {
return e
}
if errors.Is(err, fork_graph.ErrStateNotFound) {
return NewEndpointError(http.StatusNotFound, "Could not find beacon state")
}
return NewEndpointError(http.StatusInternalServerError, err.Error())
}
func NewEndpointError(code int, message string) *EndpointError {
return &EndpointError{
Code: code,
Message: message,
}
}
func (e EndpointError) Error() string {
return fmt.Sprintf("Code %d: %s", e.Code, e.Message)
}
func (e *EndpointError) WriteTo(w http.ResponseWriter) {
w.WriteHeader(e.Code)
encErr := json.NewEncoder(w).Encode(e)
if encErr != nil {
log.Error("beaconapi failed to write json error", "err", encErr)
}
}
type EndpointHandler[T any] interface {
Handle(r *http.Request) (T, error)
}
type EndpointHandlerFunc[T any] func(r *http.Request) (T, error)
func (e EndpointHandlerFunc[T]) Handle(r *http.Request) (T, error) {
return e(r)
}
func HandleEndpointFunc[T any](h EndpointHandlerFunc[T]) http.HandlerFunc {
return HandleEndpoint[T](h)
}
func HandleEndpoint[T any](h EndpointHandler[T]) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ans, err := h.Handle(r)
if err != nil {
log.Error("beacon api request error", "err", err)
endpointError := WrapEndpointError(err)
endpointError.WriteTo(w)
return
}
// TODO: ssz handler
// TODO: potentially add a context option to buffer these
contentType := r.Header.Get("Accept")
contentTypes := strings.Split(contentType, ",")
switch {
case slices.Contains(contentTypes, "application/octet-stream"):
sszMarshaler, ok := any(ans).(ssz.Marshaler)
if !ok {
NewEndpointError(http.StatusBadRequest, "This endpoint does not support SSZ response").WriteTo(w)
return
}
// TODO: we should probably figure out some way to stream this in the future :)
encoded, err := sszMarshaler.EncodeSSZ(nil)
if err != nil {
WrapEndpointError(err).WriteTo(w)
return
}
w.Write(encoded)
case contentType == "*/*", contentType == "", slices.Contains(contentTypes, "text/html"), slices.Contains(contentTypes, "application/json"):
w.Header().Add("content-type", "application/json")
err := json.NewEncoder(w).Encode(ans)
if err != nil {
// this error is fatal, log to console
log.Error("beaconapi failed to encode json", "type", reflect.TypeOf(ans), "err", err)
}
default:
http.Error(w, "content type must be application/json or application/octet-stream", http.StatusBadRequest)
}
})
}