Skip to content
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(server/v2): Add Swagger UI support for server/v2 #23092

Open
wants to merge 28 commits into
base: main
Choose a base branch
from

Conversation

crStiv
Copy link

@crStiv crStiv commented Dec 25, 2024

Description

This PR adds Swagger UI support to the server/v2 package, providing an easy way to serve and interact with API documentation. The implementation includes a configurable handler for serving Swagger UI files and proper configuration options.

Closes: #23020

Key changes:

  • Added Swagger UI handler with proper content-type detection
  • Implemented configuration options for enabling/disabling Swagger UI
  • Added server integration with the new Swagger component
  • Maintained compatibility with existing server/v2 architecture

Author Checklist

I have...
[x] included the correct type prefix (feat) in the PR title
[x] confirmed ! in the type prefix if API or client breaking change (not needed)
[x] targeted the correct branch (main)
[x] provided a link to the relevant issue (#23020)
[x] reviewed "Files changed" and left comments if necessary
[x] included the necessary unit and integration tests
[x] added a changelog entry to CHANGELOG.md:
[x] updated the relevant documentation or specification
[x] confirmed all CI checks have passed

Reviewers Checklist

[ ] confirmed the correct type prefix in the PR title
[ ] confirmed all author checklist items have been addressed
[ ] reviewed state machine logic, API design and naming, documentation is accurate, tests and test coverage

Changed files:

  • server/v2/api/swagger/handler.go
  • server/v2/api/swagger/config.go
  • server/v2/server.go

Summary by CodeRabbit

  • New Features

    • Added Swagger UI configuration and server support.
    • Introduced new configuration options for enabling and customizing Swagger documentation.
    • Implemented a flexible Swagger server with CORS and content type handling.
  • Improvements

    • Enhanced API documentation accessibility.
    • Added robust error handling for Swagger server initialization.
    • Implemented dynamic content serving for Swagger UI files.
  • Technical Enhancements

    • Introduced generic server implementation for Swagger.
    • Added content type and caching header management for Swagger resources.

Copy link
Contributor

coderabbitai bot commented Dec 25, 2024

📝 Walkthrough

Walkthrough

The pull request introduces a new swagger package in the server/v2/api directory to provide out-of-the-box Swagger UI support for the Cosmos SDK server. The implementation includes a configurable Server struct with methods for starting and stopping the Swagger server, a handler for serving Swagger UI files, and a configuration mechanism. The changes are integrated into the SimApp initialization, allowing easy addition of Swagger documentation to applications with minimal configuration.

Changes

File Change Summary
server/v2/api/swagger/config.go Added Config struct with configuration options for Swagger, including Enable, Address, and SwaggerUI. Introduced DefaultConfig and Validate methods.
server/v2/api/swagger/handler.go Implemented swaggerHandler with ServeHTTP method to handle Swagger UI file serving, including CORS and content type handling.
server/v2/api/swagger/server.go Created generic Server struct with methods for managing Swagger server lifecycle and configuration.
simapp/v2/app.go Integrated Swagger server initialization in NewSimApp, adding Swagger UI support with default configuration.
simapp/v2/simdv2/cmd/commands.go Modified InitRootCmd to include Swagger server initialization and registration.
server/v2/api/swagger/doc.go Introduced documentation for the swagger package, including example usage and configuration options.

Assessment against linked issues

Objective Addressed Explanation
Add default Swagger support in server/v2
Provide flexible Swagger UI configuration

Possibly related PRs

Suggested reviewers

  • julienrbrt
  • kocubinski
  • hieuvubk
  • tac0turtle

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added C:server/v2 Issues related to server/v2 C:server/v2 api labels Dec 25, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef2f8d and 8930230.

📒 Files selected for processing (3)
  • server/v2/api/swagger/config.go (1 hunks)
  • server/v2/api/swagger/handler.go (1 hunks)
  • server/v2/server.go (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
server/v2/api/swagger/handler.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

server/v2/api/swagger/config.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

server/v2/server.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

🪛 golangci-lint (1.62.2)
server/v2/api/swagger/handler.go

44-44: undefined: time

(typecheck)

server/v2/api/swagger/config.go

24-24: undefined: fmt

(typecheck)

server/v2/server.go

78-78: undefined: http

(typecheck)


88-88: undefined: http

(typecheck)


251-251: s.config.API undefined (type ServerConfig has no field or method API)

(typecheck)

🔇 Additional comments (5)
server/v2/api/swagger/config.go (2)

5-11: Struct fields look good
Your Config struct is straightforward and clearly documents each field. This aligns well with Go style guidelines.


13-19: Clarity in default values
Returning Enable: false and Path: "/swagger" as defaults appears consistent with the code’s intended usage. This ensures that Swagger UI is turned off by default unless explicitly enabled.

server/v2/api/swagger/handler.go (1)

12-46: Handler logic and MIME handling
Your approach to handle file serving, MIME type detection, and fallback to index.html is solid. The usage of path.Clean is sensible for preventing tricky path manipulations. Once the time package is imported, http.ServeContent is a good fit for robust file serving.

🧰 Tools
🪛 golangci-lint (1.62.2)

44-44: undefined: time

(typecheck)

server/v2/server.go (2)

Line range hint 78-88: HTTP router usage
Defining router *http.ServeMux and assigning it with http.NewServeMux() is sensible for isolating routes. Confirm the design pattern for hooking in additional routes from other components if needed.

🧰 Tools
🪛 golangci-lint (1.62.2)

78-78: undefined: http

(typecheck)


249-263: Potential config mismatch
Line 251 references s.config.API.Swagger, but the static analysis suggests that ServerConfig has no field API. Verify your ServerConfig type to ensure it holds the API field or reference the correct nested structure.

🧰 Tools
🪛 golangci-lint (1.62.2)

251-251: s.config.API undefined (type ServerConfig has no field or method API)

(typecheck)

Comment on lines 21 to 27
// Validate validates the configuration
func (c Config) Validate() error {
if c.Path == "" {
return fmt.Errorf("swagger path cannot be empty")
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure error formatting is enabled
While the validation logic is correct, remember that fmt.Errorf("swagger path cannot be empty") requires "fmt" to be imported (as noted above). Once the import is added, this code works as intended.

🧰 Tools
🪛 golangci-lint (1.62.2)

24-24: undefined: fmt

(typecheck)

Comment on lines 1 to 4
package swagger

import "github.com/cosmos/cosmos-sdk/server/v2/config"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Package import oversight
Currently, you're importing "github.com/cosmos/cosmos-sdk/server/v2/config" but using fmt.Errorf in the code without importing "fmt". There's no direct usage of the imported "github.com/cosmos/cosmos-sdk/server/v2/config" in this snippet. If it's needed in other parts of the file, that's fine. Otherwise, consider adding the missing "fmt" import or removing unused imports.

 package swagger

-import "github.com/cosmos/cosmos-sdk/server/v2/config"
+import (
+    "fmt"
+    "github.com/cosmos/cosmos-sdk/server/v2/config"
+)
 

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 3 to 10
import (
"net/http"
"path"
"path/filepath"
"strings"

"github.com/rakyll/statik/fs"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing time import
You're calling time.Time{} in this file but haven't imported "time". Add the missing import to avoid compilation issues.

 import (
     "net/http"
     "path"
     "path/filepath"
     "strings"

     "github.com/rakyll/statik/fs"
+    "time"
 )

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 11 to 18
"github.com/cosmos/cosmos-sdk/server/v2/api/swagger"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"cosmossdk.io/core/transaction"
"cosmossdk.io/log"
"github.com/rakyll/statik/fs"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Check for missing imports
You reference swagger and statik/fs but also rely on http.ServeMux later (lines 78, 88) without importing "net/http". Ensure all required imports are present to avoid compilation errors.

 import (
     "context"
     "errors"
     "fmt"
     "os"
     "path/filepath"
     "strings"

+    "net/http"
     "github.com/cosmos/cosmos-sdk/server/v2/api/swagger"
     "github.com/pelletier/go-toml/v2"
     "github.com/spf13/cobra"

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for tackling this. The wiring shouldn't be done in the main server, but api/swagger should be its own server component. Check out the other servers.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for tackling this. The wiring shouldn't be done in the main server, but api/swagger should be its own server component. Check out the other servers.

oh you're absolutely right, gonna fix it soon!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julienrbrt sorry for mess in commits, I had to redo some things but now everything should be fine

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/v2/api/swagger/config.go (1)

4-4: Remove unused imports.

The import "cosmossdk.io/core/server" is not needed and can be safely removed to adhere to Go’s best practices on unused imports.

Here’s a suggested fix:

 import (
-    "cosmossdk.io/core/server"
 )
🧰 Tools
🪛 golangci-lint (1.62.2)

4-4: "cosmossdk.io/core/server" imported and not used

(typecheck)

📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8930230 and d0c20bc.

📒 Files selected for processing (2)
  • server/v2/api/swagger/config.go (1 hunks)
  • server/v2/api/swagger/server.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
server/v2/api/swagger/config.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

server/v2/api/swagger/server.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

🪛 golangci-lint (1.62.2)
server/v2/api/swagger/config.go

4-4: "cosmossdk.io/core/server" imported and not used

(typecheck)

server/v2/api/swagger/server.go

31-31: srv.Config undefined (type *Server[T] has no field or method Config, but does have field config)

(typecheck)


40-40: undefined: NewSwaggerHandler

(typecheck)

srv.config = serverCfg

mux := http.NewServeMux()
mux.Handle(srv.config.Path, NewSwaggerHandler())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle the undefined NewSwaggerHandler reference.

The function NewSwaggerHandler() is not defined in the reviewed code, nor is it imported from any package. Ensure that NewSwaggerHandler, or whichever handler function you’re calling, is properly defined and imported to prevent a runtime error or undefined symbol.

Would you like assistance creating a NewSwaggerHandler() function?

🧰 Tools
🪛 golangci-lint (1.62.2)

40-40: undefined: NewSwaggerHandler

(typecheck)

server/v2/api/swagger/server.go Outdated Show resolved Hide resolved
server/v2/api/swagger/handler.go Fixed Show fixed Hide fixed
server/v2/api/swagger/handler.go Fixed Show fixed Hide fixed
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))

// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))

Check warning

Code scanning / CodeQL

Calling the system time Warning

Calling the system time may be a possible source of non-determinism
@julienrbrt julienrbrt changed the title feat(server): Add Swagger UI support for server/v2 feat(server/v2): Add Swagger UI support for server/v2 Dec 28, 2024
)

// Handler returns an HTTP handler for Swagger UI
func Handler() http.Handler {
Copy link
Member

@julienrbrt julienrbrt Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's not expose this and just use swaggerHandler{} in the handler registration

Copy link
Member

@julienrbrt julienrbrt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The swagger ui is passed nowhere, so this server will actually do nothing.
We should let the user pass its chain swagger ui / file at server instantiation.
For instance, a user could pass the default SDK swagger config (docs.SwaggerUI):

root, err := fs.Sub(docs.SwaggerUI, "swagger-ui")

@julienrbrt
Copy link
Member

Additionally, let's wire this server in simapp/v2 so we can test it out and be shown to users how it is wired.

@crStiv
Copy link
Author

crStiv commented Jan 6, 2025

Additionally, let's wire this server in simapp/v2 so we can test it out and be shown to users how it is wired.

@julienrbrt hey Julien, I've added some changes, is it what you asked for?

@julienrbrt
Copy link
Member

Additionally, let's wire this server in simapp/v2 so we can test it out and be shown to users how it is wired.

@julienrbrt hey Julien, I've added some changes, is it what you asked for?

Yes great, if you can create a doc.go showing how to wire this server/v2 and update simapp/v2 to wire it, then it looks good to me.

@crStiv
Copy link
Author

crStiv commented Jan 6, 2025

@julienrbrt made an update, is this doc pretty clear to understand?

@crStiv
Copy link
Author

crStiv commented Jan 6, 2025

@julienrbrt like this?

@julienrbrt
Copy link
Member

Yes, like that great work! Could you update the doc.go again? For the handler I think we can remove some headers, they aren't all necessary, let's keep the strict minimum.

@julienrbrt
Copy link
Member

Additionally can you run make lint-fix to please the linter?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/v2/api/swagger/handler.go (1)

52-72: Consider using mime.TypeByExtension for more robust content type detection.

While the current implementation covers common file types, using the standard library's mime package would provide more comprehensive coverage.

 // getContentType returns the content-type for a file extension
 func getContentType(ext string) string {
+    if mimeType := mime.TypeByExtension(ext); mimeType != "" {
+        return mimeType
+    }
+    // Fallback to current implementation for specific overrides
     switch strings.ToLower(ext) {
     case ".html":
         return "text/html"
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2021911 and 93dc4b6.

📒 Files selected for processing (1)
  • server/v2/api/swagger/handler.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
server/v2/api/swagger/handler.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Summary
🔇 Additional comments (2)
server/v2/api/swagger/handler.go (2)

1-10: LGTM! Package and imports are well-organized.

The package name follows Go conventions, and all imports are necessary and properly used.


11-13: Keep the handler struct unexported.

The struct is correctly unexported as swaggerHandler, following the previous review suggestion.

Comment on lines 24 to 35
// Process the path
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
if urlPath == "" || urlPath == "/" {
urlPath = "/index.html"
}

// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add path sanitization to prevent directory traversal.

The current path handling could be vulnerable to directory traversal attacks. Consider using filepath.Clean and additional validation.

 // Process the path
 urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
+urlPath = filepath.Clean(urlPath)
+if strings.Contains(urlPath, "..") {
+    http.Error(w, "Invalid path", http.StatusBadRequest)
+    return
+}
 if urlPath == "" || urlPath == "/" {
     urlPath = "/index.html"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Process the path
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
if urlPath == "" || urlPath == "/" {
urlPath = "/index.html"
}
// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
// Process the path
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
urlPath = filepath.Clean(urlPath)
if strings.Contains(urlPath, "..") {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
if urlPath == "" || urlPath == "/" {
urlPath = "/index.html"
}
// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}

Comment on lines 44 to 49
// Set caching headers
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))

// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize caching implementation and add security headers.

  1. Use a constant time for Last-Modified to ensure deterministic behavior
  2. Add security headers to protect against common web vulnerabilities
 // Set caching headers
 w.Header().Set("Cache-Control", "public, max-age=31536000")
-w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
+// Use a fixed timestamp for deterministic behavior
+w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
+
+// Add security headers
+w.Header().Set("X-Content-Type-Options", "nosniff")
+w.Header().Set("X-Frame-Options", "DENY")
+w.Header().Set("Content-Security-Policy", "default-src 'self'")

-http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
+http.ServeContent(w, r, urlPath, time.Unix(0, 0), file.(io.ReadSeeker))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Set caching headers
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
// Set caching headers
w.Header().Set("Cache-Control", "public, max-age=31536000")
// Use a fixed timestamp for deterministic behavior
w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
// Serve the file
http.ServeContent(w, r, urlPath, time.Unix(0, 0), file.(io.ReadSeeker))

@crStiv
Copy link
Author

crStiv commented Jan 7, 2025

@julienrbrt I've made things:

  1. Simplified CORS headers
  2. Updated doc.go
  3. Added path validation in handler.go for security
  4. Ran make lint-fix - no issues found

If anything else, I'm open to do it

@crStiv
Copy link
Author

crStiv commented Jan 7, 2025

still checks are red

@@ -163,6 +166,18 @@ func InitRootCmd[T transaction.Tx](
}
registerGRPCGatewayRoutes[T](deps, grpcgatewayServer)

// Create Swagger server
swaggerServer, err := swaggerv2.New[T](
logger.With(log.ModuleKey, "swagger"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just pass the litter here, the key is added in the constrictor

@@ -38,6 +38,8 @@ import (
genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
v2 "github.com/cosmos/cosmos-sdk/x/genutil/v2/cli"
"cosmossdk.io/client/docs"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import doesn't exist and the ordering is wrong (Make lint-fix should have fixed that.)


// Create Swagger server
swaggerServer, err := swaggerv2.New[T](
logger.With(log.ModuleKey, "swagger"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

return &Config{
Enable: true,
Address: "localhost:8080",
Path: "/swagger/",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a path? I think keeping /swagger by default is great.


// Config defines the configuration for the Swagger UI server
type Config struct {
Enable bool `toml:"enable" mapstructure:"enable"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add toml comment annotation

func DefaultConfig() *Config {
return &Config{
Enable: true,
Address: "localhost:8080",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use something else than 8080 by default

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
server/v2/api/swagger/config.go (2)

3-8: Remove unused import

The "cosmossdk.io/core/server" package is imported but not used in this file.

 import (
     "fmt"
     "net/http"
-
-    "cosmossdk.io/core/server"
 )

12-20: Enhance struct documentation

While the struct is well-documented, consider adding more details about:

  • Expected format for the Address field (e.g., "host:port")
  • Default values
  • Usage examples
server/v2/api/swagger/handler.go (2)

24-38: Enhance path validation

While the current path validation is good, consider adding:

  1. Maximum path length check
  2. Validation for allowed characters
 // Process and validate the path
 urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
+
+// Check path length
+if len(urlPath) > 255 {
+    http.Error(w, "Path too long", http.StatusBadRequest)
+    return
+}
+
+// Check for allowed characters
+if strings.ContainsAny(urlPath, "<>:\"\\|?*") {
+    http.Error(w, "Invalid characters in path", http.StatusBadRequest)
+    return
+}

62-81: Add default content type

Consider adding a default content type for unknown extensions to ensure proper content handling.

 func getContentType(ext string) string {
     switch strings.ToLower(ext) {
         // ... existing cases ...
     default:
-        return ""
+        return "application/octet-stream"
     }
 }
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93dc4b6 and 0938d61.

📒 Files selected for processing (3)
  • server/v2/api/swagger/config.go (1 hunks)
  • server/v2/api/swagger/doc.go (1 hunks)
  • server/v2/api/swagger/handler.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/v2/api/swagger/doc.go
🧰 Additional context used
📓 Path-based instructions (2)
server/v2/api/swagger/config.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

server/v2/api/swagger/handler.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

🪛 golangci-lint (1.62.2)
server/v2/api/swagger/config.go

1-1: : # cosmossdk.io/server/v2/api/swagger
api/swagger/server.go:51:27: srv.config.Path undefined (type *Config has no field or method Path)
api/swagger/config.go:7:5: "cosmossdk.io/core/server" imported and not used

(typecheck)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Summary
🔇 Additional comments (5)
server/v2/api/swagger/config.go (2)

22-28: LGTM! Good choice of default port

The default configuration uses port 8090, which is a good choice as it avoids conflicts with commonly used ports.


30-41: LGTM! Proper validation logic

The validation logic correctly checks for required fields when Swagger UI is enabled.

server/v2/api/swagger/handler.go (3)

11-13: LGTM! Clean handler implementation

The handler struct is minimal and follows good design practices.


53-58: ⚠️ Potential issue

Use deterministic timestamps

Using time.Now() can lead to non-deterministic behavior. Consider using a fixed timestamp for caching headers.

 // Set caching headers
 w.Header().Set("Cache-Control", "public, max-age=31536000")
-w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
+// Use a fixed timestamp for deterministic behavior
+w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")

-http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
+http.ServeContent(w, r, urlPath, time.Unix(0, 0), file.(io.ReadSeeker))

Likely invalid or redundant comment.


16-22: 🛠️ Refactor suggestion

Strengthen CORS security

The current CORS configuration is too permissive for production use. Consider:

  1. Restricting allowed origins
  2. Adding additional security headers
 // Set minimal CORS headers
-w.Header().Set("Access-Control-Allow-Origin", "*")
+w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
 w.Header().Set("Access-Control-Allow-Methods", "GET")
+w.Header().Set("X-Content-Type-Options", "nosniff")
+w.Header().Set("X-Frame-Options", "DENY")

Likely invalid or redundant comment.

@crStiv
Copy link
Author

crStiv commented Jan 7, 2025

@julienrbrt I ran a linter, there are 0 problems but there is a text in terminal like this

linting module cosmossdk.io/server/v2/stf [2025-01-07T17:19:55+00:00]
make: *** [scripts/build/linting.mk:28: lint-fix] Error 123

@julienrbrt
Copy link
Member

Hey, this is because of the build failure. You need to make sure that the app builds.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
server/v2/api/swagger/handler.go (2)

47-54: Add caching headers and use deterministic timestamps.

The current implementation:

  1. Lacks caching headers for static files.
  2. Uses non-deterministic time.Now() which could cause issues in tests.

Apply this diff:

     // Set the content-type
     ext := filepath.Ext(urlPath)
     if ct := getContentType(ext); ct != "" {
         w.Header().Set("Content-Type", ct)
     }
 
+    // Add caching headers for static files
+    w.Header().Set("Cache-Control", "public, max-age=3600")
+    w.Header().Set("ETag", fmt.Sprintf("W/\"%x\"", sha256.Sum256([]byte(urlPath))))
+
     // Serve the file
-    http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
+    http.ServeContent(w, r, urlPath, time.Unix(0, 0), file.(io.ReadSeeker))

58-77: Enhance content type detection.

Consider these improvements:

  1. Use mime.TypeByExtension as a fallback for unknown extensions.
  2. Add support for additional Swagger UI file types (yaml, ico).

Apply this diff:

+import "mime"

 func getContentType(ext string) string {
-    switch strings.ToLower(ext) {
-    case ".html":
-        return "text/html"
-    case ".css":
-        return "text/css"
-    case ".js":
-        return "application/javascript"
-    case ".json":
-        return "application/json"
-    case ".png":
-        return "image/png"
-    case ".jpg", ".jpeg":
-        return "image/jpeg"
-    case ".svg":
-        return "image/svg+xml"
-    default:
-        return ""
+    // Handle known types first
+    knownTypes := map[string]string{
+        ".html": "text/html",
+        ".css":  "text/css",
+        ".js":   "application/javascript",
+        ".json": "application/json",
+        ".yaml": "application/yaml",
+        ".yml":  "application/yaml",
+        ".png":  "image/png",
+        ".jpg":  "image/jpeg",
+        ".jpeg": "image/jpeg",
+        ".svg":  "image/svg+xml",
+        ".ico":  "image/x-icon",
     }
+    
+    if ct, ok := knownTypes[strings.ToLower(ext)]; ok {
+        return ct
+    }
+    
+    // Fallback to standard mime package
+    if ct := mime.TypeByExtension(ext); ct != "" {
+        return ct
+    }
+    
+    return ""
 }
simapp/v2/simdv2/cmd/commands.go (2)

18-18: Fix import ordering.

The import statement for swagger package is causing linting issues.

Apply this diff to fix the import ordering:

-	swaggerv2 "cosmossdk.io/server/v2/api/swagger"
 	"cosmossdk.io/server/v2/api/telemetry"
 	"cosmossdk.io/server/v2/cometbft"
 	serverstore "cosmossdk.io/server/v2/store"
+	swaggerv2 "cosmossdk.io/server/v2/api/swagger"
🧰 Tools
🪛 golangci-lint (1.62.2)

18-18: could not import cosmossdk.io/server/v2/api/swagger (-: # cosmossdk.io/server/v2/api/swagger
../../server/v2/api/swagger/config.go:7:5: "cosmossdk.io/core/server" imported and not used)

(typecheck)


169-175: Simplify logger creation.

The logger key is already added in the constructor, no need to add it here.

Apply this diff:

-	swaggerServer, err := swaggerv2.New[T](
-		logger.With(log.ModuleKey, "swagger"),
-		deps.GlobalConfig,
-	)
+	swaggerServer, err := swaggerv2.New[T](logger, deps.GlobalConfig)
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0938d61 and 8f3017c.

📒 Files selected for processing (5)
  • server/v2/api/swagger/doc.go (1 hunks)
  • server/v2/api/swagger/handler.go (1 hunks)
  • server/v2/api/swagger/server.go (1 hunks)
  • simapp/v2/app.go (1 hunks)
  • simapp/v2/simdv2/cmd/commands.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/v2/api/swagger/doc.go
  • server/v2/api/swagger/server.go
🧰 Additional context used
📓 Path-based instructions (3)
server/v2/api/swagger/handler.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

simapp/v2/app.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

simapp/v2/simdv2/cmd/commands.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

🪛 golangci-lint (1.62.2)
simapp/v2/simdv2/cmd/commands.go

18-18: could not import cosmossdk.io/server/v2/api/swagger (-: # cosmossdk.io/server/v2/api/swagger
../../server/v2/api/swagger/config.go:7:5: "cosmossdk.io/core/server" imported and not used)

(typecheck)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Summary
🔇 Additional comments (1)
server/v2/api/swagger/handler.go (1)

11-13: LGTM!

The struct design follows good encapsulation principles by being unexported and using the appropriate http.FileSystem interface for serving static files.

Comment on lines 15 to 55
func (h *swaggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set minimal CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")

if r.Method == http.MethodOptions {
return
}

// Process and validate the path
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
if urlPath == "" || urlPath == "/" {
urlPath = "/index.html"
}

// Basic path validation
if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}

// Clean the path
urlPath = filepath.Clean(urlPath)

// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer file.Close()

// Set the content-type
ext := filepath.Ext(urlPath)
if ct := getContentType(ext); ct != "" {
w.Header().Set("Content-Type", ct)
}

// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Strengthen path validation and add security headers.

While basic path validation exists, it could be enhanced:

  1. Use filepath.Clean before validation to prevent bypass attempts.
  2. Add security headers to protect against common web vulnerabilities.

Apply this diff:

 func (h *swaggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     // Set minimal CORS headers
     w.Header().Set("Access-Control-Allow-Origin", "*")
     w.Header().Set("Access-Control-Allow-Methods", "GET")
+    // Add security headers
+    w.Header().Set("X-Content-Type-Options", "nosniff")
+    w.Header().Set("X-Frame-Options", "DENY")
+    w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'")

     if r.Method == http.MethodOptions {
         return
     }

     // Process and validate the path
     urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
+    urlPath = filepath.Clean(urlPath)
+    
+    // Validate path before any operations
+    if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") || strings.Contains(urlPath, "\x00") {
         http.Error(w, "Invalid path", http.StatusBadRequest)
         return
     }
-
-    // Clean the path
-    urlPath = filepath.Clean(urlPath)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (h *swaggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set minimal CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
if r.Method == http.MethodOptions {
return
}
// Process and validate the path
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
if urlPath == "" || urlPath == "/" {
urlPath = "/index.html"
}
// Basic path validation
if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
// Clean the path
urlPath = filepath.Clean(urlPath)
// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer file.Close()
// Set the content-type
ext := filepath.Ext(urlPath)
if ct := getContentType(ext); ct != "" {
w.Header().Set("Content-Type", ct)
}
// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
}
func (h *swaggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set minimal CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'")
if r.Method == http.MethodOptions {
return
}
// Process and validate the path
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
urlPath = filepath.Clean(urlPath)
// Validate path before any operations
if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") || strings.Contains(urlPath, "\x00") {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
if urlPath == "" || urlPath == "/" {
urlPath = "/index.html"
}
// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer file.Close()
// Set the content-type
ext := filepath.Ext(urlPath)
if ct := getContentType(ext); ct != "" {
w.Header().Set("Content-Type", ct)
}
// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
server/v2/api/swagger/handler.go (2)

29-42: Strengthen path validation and improve error messaging.

While the path validation is good, it could be enhanced:

  1. Add validation for null bytes and other malicious characters
  2. Provide more descriptive error messages
  3. Consider logging invalid path attempts
 // Process and validate the path
 urlPath := strings.TrimPrefix(r.URL.Path, "/swagger")
+
+// Log the requested path for debugging
+h.logger.Debug("swagger request path", "path", urlPath)
+
 if urlPath == "" || urlPath == "/" {
     urlPath = "/index.html"
 }

 // Clean the path before validation
 urlPath = filepath.Clean(urlPath)

 // Validate path before any operations
-if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") || strings.Contains(urlPath, "\\") {
-    http.Error(w, "Invalid path", http.StatusBadRequest)
+if strings.Contains(urlPath, "..") || strings.Contains(urlPath, "//") || 
+   strings.Contains(urlPath, "\\") || strings.Contains(urlPath, "\x00") {
+    msg := "Invalid path: contains forbidden characters or sequences"
+    h.logger.Warn("invalid swagger path attempt", "path", urlPath)
+    http.Error(w, msg, http.StatusBadRequest)
     return
 }

62-82: Enhance content type mapping.

Consider adding support for additional common file types and using standard MIME types:

 func getContentType(ext string) string {
     switch strings.ToLower(ext) {
     case ".html":
-        return "text/html"
+        return "text/html; charset=utf-8"
     case ".css":
-        return "text/css"
+        return "text/css; charset=utf-8"
     case ".js":
-        return "application/javascript"
+        return "application/javascript; charset=utf-8"
     case ".json":
-        return "application/json"
+        return "application/json; charset=utf-8"
     case ".png":
         return "image/png"
     case ".jpg", ".jpeg":
         return "image/jpeg"
     case ".svg":
-        return "image/svg+xml"
+        return "image/svg+xml; charset=utf-8"
+    case ".ico":
+        return "image/x-icon"
+    case ".woff":
+        return "font/woff"
+    case ".woff2":
+        return "font/woff2"
     default:
         return ""
     }
 }
server/v2/api/swagger/server.go (1)

71-81: Improve configuration handling with validation.

The configuration retrieval should include validation when creating a new config:

 func (s *Server[T]) Config() any {
     if s.config == nil {
         cfg := DefaultConfig()
         for _, opt := range s.cfgOptions {
             opt(cfg)
         }
+        // Validate the configuration
+        if err := cfg.Validate(); err != nil {
+            s.logger.Error("invalid swagger configuration", "error", err)
+            return DefaultConfig()
+        }
         return cfg
     }
     return s.config
 }
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f3017c and 44da58f.

📒 Files selected for processing (2)
  • server/v2/api/swagger/handler.go (1 hunks)
  • server/v2/api/swagger/server.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
server/v2/api/swagger/server.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

server/v2/api/swagger/handler.go (1)

Pattern **/*.go: Review the Golang code for conformity with the Uber Golang style guide, highlighting any deviations.

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Summary

Comment on lines +16 to +23
// Set minimal CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")

// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance CORS and security headers configuration.

The current CORS configuration is too permissive. Consider:

  1. Restricting CORS to specific origins instead of "*"
  2. Adding additional security headers for better protection
 // Set minimal CORS headers
-w.Header().Set("Access-Control-Allow-Origin", "*")
+// TODO: Make this configurable via Config struct
+w.Header().Set("Access-Control-Allow-Origin", "https://api.example.com")
 w.Header().Set("Access-Control-Allow-Methods", "GET")
+w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

 // Add security headers
 w.Header().Set("X-Content-Type-Options", "nosniff")
 w.Header().Set("X-Frame-Options", "DENY")
 w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'")
+w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Set minimal CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'")
// Set minimal CORS headers
// TODO: Make this configurable via Config struct
w.Header().Set("Access-Control-Allow-Origin", "https://api.example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// Add security headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")

Comment on lines +44 to +60
// Open the file
file, err := h.swaggerFS.Open(urlPath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer file.Close()

// Set the content-type
ext := filepath.Ext(urlPath)
if ct := getContentType(ext); ct != "" {
w.Header().Set("Content-Type", ct)
}

// Serve the file
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve file serving and error handling.

The current implementation has two issues:

  1. Using time.Now() in ServeContent is non-deterministic
  2. Error handling could be more informative
 // Open the file
 file, err := h.swaggerFS.Open(urlPath)
 if err != nil {
-    http.Error(w, "File not found", http.StatusNotFound)
+    msg := fmt.Sprintf("Swagger file not found: %s", urlPath)
+    h.logger.Debug("swagger file not found", "path", urlPath, "error", err)
+    http.Error(w, msg, http.StatusNotFound)
     return
 }
 defer file.Close()

 // Set the content-type
 ext := filepath.Ext(urlPath)
 if ct := getContentType(ext); ct != "" {
     w.Header().Set("Content-Type", ct)
 }

 // Serve the file
-http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker))
+// Use a fixed timestamp for deterministic behavior
+http.ServeContent(w, r, urlPath, time.Unix(0, 0), file.(io.ReadSeeker))

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +83 to +106
// Start starts the server
func (s *Server[T]) Start(ctx context.Context) error {
if !s.config.Enable {
s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name()))
return nil
}

s.logger.Info("starting swagger server...", "address", s.config.Address)
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("failed to start swagger server: %w", err)
}

return nil
}

// Stop stops the server
func (s *Server[T]) Stop(ctx context.Context) error {
if !s.config.Enable {
return nil
}

s.logger.Info("stopping swagger server...", "address", s.config.Address)
return s.server.Shutdown(ctx)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance server lifecycle handling with context and graceful shutdown.

The server lifecycle management should:

  1. Use context in Start method
  2. Add graceful shutdown timeout
  3. Improve error handling
 func (s *Server[T]) Start(ctx context.Context) error {
     if !s.config.Enable {
         s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name()))
         return nil
     }

     s.logger.Info("starting swagger server...", "address", s.config.Address)
-    if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
-        return fmt.Errorf("failed to start swagger server: %w", err)
+    
+    // Use errChan to handle server errors
+    errChan := make(chan error, 1)
+    go func() {
+        if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+            errChan <- fmt.Errorf("failed to start swagger server: %w", err)
+        }
+    }()
+
+    // Wait for context cancellation or server error
+    select {
+    case <-ctx.Done():
+        return s.Stop(context.Background())
+    case err := <-errChan:
+        return err
     }

     return nil
 }

 func (s *Server[T]) Stop(ctx context.Context) error {
     if !s.config.Enable {
         return nil
     }

     s.logger.Info("stopping swagger server...", "address", s.config.Address)
-    return s.server.Shutdown(ctx)
+    
+    // Create a timeout context for shutdown
+    shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+    defer cancel()
+
+    if err := s.server.Shutdown(shutdownCtx); err != nil {
+        s.logger.Error("failed to gracefully shutdown swagger server", "error", err)
+        return fmt.Errorf("failed to gracefully shutdown swagger server: %w", err)
+    }
+    
+    return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Start starts the server
func (s *Server[T]) Start(ctx context.Context) error {
if !s.config.Enable {
s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name()))
return nil
}
s.logger.Info("starting swagger server...", "address", s.config.Address)
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("failed to start swagger server: %w", err)
}
return nil
}
// Stop stops the server
func (s *Server[T]) Stop(ctx context.Context) error {
if !s.config.Enable {
return nil
}
s.logger.Info("stopping swagger server...", "address", s.config.Address)
return s.server.Shutdown(ctx)
}
// Start starts the server
func (s *Server[T]) Start(ctx context.Context) error {
if !s.config.Enable {
s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name()))
return nil
}
s.logger.Info("starting swagger server...", "address", s.config.Address)
// Use errChan to handle server errors
errChan := make(chan error, 1)
go func() {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errChan <- fmt.Errorf("failed to start swagger server: %w", err)
}
}()
// Wait for context cancellation or server error
select {
case <-ctx.Done():
return s.Stop(context.Background())
case err := <-errChan:
return err
}
return nil
}
// Stop stops the server
func (s *Server[T]) Stop(ctx context.Context) error {
if !s.config.Enable {
return nil
}
s.logger.Info("stopping swagger server...", "address", s.config.Address)
// Create a timeout context for shutdown
shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := s.server.Shutdown(shutdownCtx); err != nil {
s.logger.Error("failed to gracefully shutdown swagger server", "error", err)
return fmt.Errorf("failed to gracefully shutdown swagger server: %w", err)
}
return nil
}

Comment on lines +19 to +64
// Server represents a Swagger UI server
type Server[T transaction.Tx] struct {
logger log.Logger
config *Config
cfgOptions []CfgOption
server *http.Server
}

// New creates a new Swagger UI server
func New[T transaction.Tx](
logger log.Logger,
cfg server.ConfigMap,
cfgOptions ...CfgOption,
) (*Server[T], error) {
srv := &Server[T]{
logger: logger.With(log.ModuleKey, ServerName),
cfgOptions: cfgOptions,
}

serverCfg := DefaultConfig()
if len(cfg) > 0 {
if err := serverv2.UnmarshalSubConfig(cfg, ServerName, serverCfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
}
for _, opt := range cfgOptions {
opt(serverCfg)
}
srv.config = serverCfg

if err := srv.config.Validate(); err != nil {
return nil, err
}

mux := http.NewServeMux()
mux.Handle("/swagger", &swaggerHandler{
swaggerFS: srv.config.SwaggerUI,
})

srv.server = &http.Server{
Addr: srv.config.Address,
Handler: mux,
}

return srv, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance server initialization with timeouts and improved error handling.

The server initialization should include:

  1. HTTP server timeouts for better security
  2. More detailed error handling
  3. Graceful shutdown configuration
 srv.server = &http.Server{
     Addr:    srv.config.Address,
     Handler: mux,
+    // Add timeouts for better security
+    ReadTimeout:       15 * time.Second,
+    ReadHeaderTimeout: 5 * time.Second,
+    WriteTimeout:      15 * time.Second,
+    IdleTimeout:       60 * time.Second,
+    MaxHeaderBytes:    1 << 20, // 1MB
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Server represents a Swagger UI server
type Server[T transaction.Tx] struct {
logger log.Logger
config *Config
cfgOptions []CfgOption
server *http.Server
}
// New creates a new Swagger UI server
func New[T transaction.Tx](
logger log.Logger,
cfg server.ConfigMap,
cfgOptions ...CfgOption,
) (*Server[T], error) {
srv := &Server[T]{
logger: logger.With(log.ModuleKey, ServerName),
cfgOptions: cfgOptions,
}
serverCfg := DefaultConfig()
if len(cfg) > 0 {
if err := serverv2.UnmarshalSubConfig(cfg, ServerName, serverCfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
}
for _, opt := range cfgOptions {
opt(serverCfg)
}
srv.config = serverCfg
if err := srv.config.Validate(); err != nil {
return nil, err
}
mux := http.NewServeMux()
mux.Handle("/swagger", &swaggerHandler{
swaggerFS: srv.config.SwaggerUI,
})
srv.server = &http.Server{
Addr: srv.config.Address,
Handler: mux,
}
return srv, nil
}
// Server represents a Swagger UI server
type Server[T transaction.Tx] struct {
logger log.Logger
config *Config
cfgOptions []CfgOption
server *http.Server
}
// New creates a new Swagger UI server
func New[T transaction.Tx](
logger log.Logger,
cfg server.ConfigMap,
cfgOptions ...CfgOption,
) (*Server[T], error) {
srv := &Server[T]{
logger: logger.With(log.ModuleKey, ServerName),
cfgOptions: cfgOptions,
}
serverCfg := DefaultConfig()
if len(cfg) > 0 {
if err := serverv2.UnmarshalSubConfig(cfg, ServerName, serverCfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
}
for _, opt := range cfgOptions {
opt(serverCfg)
}
srv.config = serverCfg
if err := srv.config.Validate(); err != nil {
return nil, err
}
mux := http.NewServeMux()
mux.Handle("/swagger", &swaggerHandler{
swaggerFS: srv.config.SwaggerUI,
})
srv.server = &http.Server{
Addr: srv.config.Address,
Handler: mux,
// Add timeouts for better security
ReadTimeout: 15 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
return srv, nil
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C:server/v2 api C:server/v2 Issues related to server/v2
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Default Swagger support in server/v2?
2 participants