-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: wire v2 handlers #22112
base: main
Are you sure you want to change the base?
feat: wire v2 handlers #22112
Changes from 8 commits
233a249
d7202e4
f1d7ebe
75e1924
86d450f
f0af908
22b9f92
45b5d74
a67fcc9
3007e67
b4027dd
85bd55a
f22a135
5d05234
ee8d0a4
40b5bae
52d57cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package runtime | ||
|
||
import ( | ||
"testing" | ||
|
||
appmodulev2 "cosmossdk.io/core/appmodule/v2" | ||
"cosmossdk.io/core/transaction" | ||
"cosmossdk.io/server/v2/stf" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
// MockModule implements both HasMsgHandlers and HasQueryHandlers | ||
type MockModule struct { | ||
mock.Mock | ||
appmodulev2.AppModule | ||
} | ||
|
||
func (m *MockModule) RegisterMsgHandlers(router appmodulev2.MsgRouter) { | ||
m.Called(router) | ||
} | ||
|
||
func (m *MockModule) RegisterQueryHandlers(router appmodulev2.QueryRouter) { | ||
m.Called(router) | ||
} | ||
|
||
func TestRegisterServices(t *testing.T) { | ||
mockModule := new(MockModule) | ||
|
||
app := &App[transaction.Tx]{ | ||
msgRouterBuilder: stf.NewMsgRouterBuilder(), | ||
queryRouterBuilder: stf.NewMsgRouterBuilder(), | ||
} | ||
|
||
mm := &MM[transaction.Tx]{ | ||
modules: map[string]appmodulev2.AppModule{ | ||
"mock": mockModule, | ||
}, | ||
} | ||
|
||
mockModule.On("RegisterMsgHandlers", app.msgRouterBuilder).Once() | ||
mockModule.On("RegisterQueryHandlers", app.queryRouterBuilder).Once() | ||
|
||
err := mm.RegisterServices(app) | ||
|
||
assert.NoError(t, err) | ||
|
||
mockModule.AssertExpectations(t) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package rest | ||
|
||
func DefaultConfig() *Config { | ||
return &Config{ | ||
Enable: true, | ||
Address: "localhost:8080", | ||
} | ||
} | ||
|
||
type CfgOption func(*Config) | ||
|
||
// Config defines configuration for the HTTP server. | ||
type Config struct { | ||
// Enable defines if the HTTP server should be enabled. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we say rest server instead of http server? so it matches the component name. |
||
Enable bool `mapstructure:"enable" toml:"enable" comment:"Enable defines if the HTTP server should be enabled."` | ||
// Address defines the API server to listen on | ||
Address string `mapstructure:"address" toml:"address" comment:"Address defines the HTTP server address to bind to."` | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||
package rest | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||||
"cosmossdk.io/core/transaction" | ||||||||||||||||||||||||||||
"cosmossdk.io/server/v2/appmanager" | ||||||||||||||||||||||||||||
"encoding/json" | ||||||||||||||||||||||||||||
"fmt" | ||||||||||||||||||||||||||||
gogoproto "github.com/cosmos/gogoproto/proto" | ||||||||||||||||||||||||||||
"github.com/gogo/protobuf/jsonpb" | ||||||||||||||||||||||||||||
"net/http" | ||||||||||||||||||||||||||||
"reflect" | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
"strings" | ||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
const ( | ||||||||||||||||||||||||||||
ContentTypeJSON = "application/json" | ||||||||||||||||||||||||||||
ContentTypeOctetStream = "application/octet-stream" | ||||||||||||||||||||||||||||
ContentTypeProtobuf = "application/x-protobuf" | ||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
type DefaultHandler[T transaction.Tx] struct { | ||||||||||||||||||||||||||||
appManager *appmanager.AppManager[T] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
func (h *DefaultHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||
path := strings.TrimPrefix(r.URL.Path, "/") | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if r.Method != http.MethodPost { | ||||||||||||||||||||||||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
contentType := r.Header.Get("Content-Type") | ||||||||||||||||||||||||||||
if contentType != ContentTypeJSON { | ||||||||||||||||||||||||||||
contentType = ContentTypeJSON | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate Currently, if the Consider validating the contentType := r.Header.Get("Content-Type")
if contentType != ContentTypeJSON {
- contentType = ContentTypeJSON
+ http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType)
+ return
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
requestType := gogoproto.MessageType(path) | ||||||||||||||||||||||||||||
if requestType == nil { | ||||||||||||||||||||||||||||
http.Error(w, "Unknown request type", http.StatusNotFound) | ||||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
msg := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Minimize the use of reflection for better performance and safety Reflection can introduce performance overhead and potential runtime errors. According to the Uber Go Style Guide, reflection should be used carefully. Consider alternative approaches to create new message instances without reflection. For example, maintain a registry or map of constructor functions keyed by message type. |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
err := jsonpb.Unmarshal(r.Body, msg) | ||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||
http.Error(w, "Error parsing body", http.StatusBadRequest) | ||||||||||||||||||||||||||||
fmt.Fprintf(w, "Error parsing body: %v\n", err) | ||||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
Comment on lines
+51
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consolidate error handling when parsing the request body Using both Modify the error handling to return a JSON response with error details: if err != nil {
- http.Error(w, "Error parsing body", http.StatusBadRequest)
- fmt.Fprintf(w, "Error parsing body: %v\n", err)
+ w.Header().Set("Content-Type", ContentTypeJSON)
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{
+ "error": fmt.Sprintf("Error parsing body: %v", err),
+ })
return
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
query, err := h.appManager.Query(r.Context(), 0, msg) | ||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||
http.Error(w, "Error querying", http.StatusInternalServerError) | ||||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
json.NewEncoder(w).Encode(query) | ||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package rest | ||
|
||
import ( | ||
"bytes" | ||
"cosmossdk.io/core/transaction" | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
func TestDefaultHandlerServeHTTP(t *testing.T) { | ||
mockAppManager := new(MockAppManager) | ||
handler := &DefaultHandler[transaction.Tx]{ | ||
appManager: mockAppManager, | ||
} | ||
|
||
body := []byte(`{"test": "data"}`) | ||
req, err := http.NewRequest("POST", "/MockMessage", bytes.NewBuffer(body)) | ||
assert.NoError(t, err) | ||
|
||
rr := httptest.NewRecorder() | ||
|
||
expectedResponse := map[string]string{"result": "success"} | ||
mockAppManager.On("Query", mock.Anything, int64(0), mock.AnythingOfType("*rest.MockMessage")).Return(expectedResponse, nil) | ||
|
||
handler.ServeHTTP(rr, req) | ||
|
||
assert.Equal(t, http.StatusOK, rr.Code) | ||
|
||
var response map[string]string | ||
err = json.Unmarshal(rr.Body.Bytes(), &response) | ||
assert.NoError(t, err) | ||
assert.Equal(t, expectedResponse, response) | ||
|
||
mockAppManager.AssertExpectations(t) | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,93 @@ | ||||||||||||||||||||||||||||||||
package rest | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||||||||
"context" | ||||||||||||||||||||||||||||||||
"cosmossdk.io/server/v2/appmanager" | ||||||||||||||||||||||||||||||||
"errors" | ||||||||||||||||||||||||||||||||
"net/http" | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
"github.com/gorilla/mux" | ||||||||||||||||||||||||||||||||
randygrok marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
"cosmossdk.io/core/transaction" | ||||||||||||||||||||||||||||||||
"cosmossdk.io/log" | ||||||||||||||||||||||||||||||||
serverv2 "cosmossdk.io/server/v2" | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const ( | ||||||||||||||||||||||||||||||||
ServerName = "rest-v2" | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
type Server[T transaction.Tx] struct { | ||||||||||||||||||||||||||||||||
logger log.Logger | ||||||||||||||||||||||||||||||||
router *mux.Router | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
httpServer *http.Server | ||||||||||||||||||||||||||||||||
config *Config | ||||||||||||||||||||||||||||||||
cfgOptions []CfgOption | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
func New[T transaction.Tx](cfgOptions ...CfgOption) *Server[T] { | ||||||||||||||||||||||||||||||||
return &Server[T]{ | ||||||||||||||||||||||||||||||||
cfgOptions: cfgOptions, | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
func (s *Server[T]) Name() string { | ||||||||||||||||||||||||||||||||
return ServerName | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
func (s *Server[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logger log.Logger) error { | ||||||||||||||||||||||||||||||||
s.logger = logger.With(log.ModuleKey, s.Name()) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
s.config = s.Config().(*Config) | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need the same config unmarshalling than with other servers here. |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
var appManager *appmanager.AppManager[T] | ||||||||||||||||||||||||||||||||
appManager = appI.GetAppManager() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
s.router = mux.NewRouter() | ||||||||||||||||||||||||||||||||
s.router.PathPrefix("/").Handler(&DefaultHandler[T]{ | ||||||||||||||||||||||||||||||||
appManager: appManager, | ||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
func (s *Server[T]) Start(ctx context.Context) error { | ||||||||||||||||||||||||||||||||
s.httpServer = &http.Server{ | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to add the enable check like we do in other servers here. |
||||||||||||||||||||||||||||||||
Addr: s.config.Address, | ||||||||||||||||||||||||||||||||
Handler: s.router, | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+51
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check if the server is enabled before starting Currently, the Apply this diff to add the enable check: func (s *Server[T]) Start(ctx context.Context) error {
+ if !s.config.Enable {
+ s.logger.Info("HTTP server is disabled")
+ return nil
+ }
s.httpServer = &http.Server{
Addr: s.config.Address,
Handler: s.router,
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
go func() { | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to be in a go routine, we can be blocking, server components are already running in a go routine |
||||||||||||||||||||||||||||||||
s.logger.Info("Starting HTTP server", "address", s.config.Address) | ||||||||||||||||||||||||||||||||
if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { | ||||||||||||||||||||||||||||||||
s.logger.Error("Failed to start HTTP server", "error", err) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+59
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notify the caller if the HTTP server fails to start In the goroutine, if Consider modifying the func (s *Server[T]) Start(ctx context.Context) error {
+ errChan := make(chan error, 1)
s.httpServer = &http.Server{
Addr: s.config.Address,
Handler: s.router,
}
go func() {
s.logger.Info("Starting HTTP server", "address", s.config.Address)
if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("Failed to start HTTP server", "error", err)
+ errChan <- err
} else {
+ errChan <- nil
}
}()
+ if err := <-errChan; err != nil {
+ return err
+ }
return nil
} This way, the
|
||||||||||||||||||||||||||||||||
}() | ||||||||||||||||||||||||||||||||
Comment on lines
+57
to
+62
Check notice Code scanning / CodeQL Spawning a Go routine Note
Spawning a Go routine may be a possible source of non-determinism
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
func (s *Server[T]) Stop(ctx context.Context) error { | ||||||||||||||||||||||||||||||||
if !s.config.Enable { | ||||||||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
s.logger.Info("Stopping HTTP server") | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return s.httpServer.Shutdown(ctx) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
func (s *Server[T]) Config() any { | ||||||||||||||||||||||||||||||||
if s.config == nil || s.config == (&Config{}) { | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct the comparison in the In line 78, the comparison Apply this diff to fix the comparison: func (s *Server[T]) Config() any {
- if s.config == nil || s.config == (&Config{}) {
+ if s.config == nil || *s.config == (Config{}) {
cfg := DefaultConfig()
for _, opt := range s.cfgOptions {
opt(cfg)
}
return cfg
}
return s.config
} 📝 Committable suggestion
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
bot is right |
||||||||||||||||||||||||||||||||
cfg := DefaultConfig() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
for _, opt := range s.cfgOptions { | ||||||||||||||||||||||||||||||||
opt(cfg) | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return cfg | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return s.config | ||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package rest | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"cosmossdk.io/core/transaction" | ||
) | ||
|
||
func TestServerConfig(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
setupFunc func() *Config | ||
expectedConfig *Config | ||
}{ | ||
{ | ||
name: "Default configuration, no custom configuration", | ||
setupFunc: func() *Config { | ||
s := New[transaction.Tx]() | ||
return s.Config().(*Config) | ||
}, | ||
expectedConfig: DefaultConfig(), | ||
}, | ||
{ | ||
name: "Custom configuration", | ||
setupFunc: func() *Config { | ||
s := New[transaction.Tx](func(config *Config) { | ||
config.Enable = false | ||
}) | ||
return s.Config().(*Config) | ||
}, | ||
expectedConfig: &Config{ | ||
Enable: false, // Custom configuration | ||
Address: "localhost:8080", | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
config := tc.setupFunc() | ||
require.Equal(t, tc.expectedConfig, config) | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no option defined, so let's delete this.