Add solution for gin challenge-2-middleware by shansing#1457
Add solution for gin challenge-2-middleware by shansing#1457RezaSi merged 1 commit intoRezaSi:mainfrom
Conversation
WalkthroughThis change introduces a complete Go web service using Gin framework with in-memory article storage, implementing a comprehensive middleware stack including request IDs, logging, authentication, CORS, rate limiting, and content-type validation, along with full CRUD handlers for articles and admin statistics endpoints. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant ErrorHandler as ErrorHandler Middleware
participant RequestID as RequestID Middleware
participant Logging as Logging Middleware
participant Auth as Auth Middleware
participant RateLimit as RateLimit Middleware
participant ContentType as ContentType Middleware
participant Handler as Route Handler
participant Storage as In-Memory Storage
Client->>ErrorHandler: HTTP Request
ErrorHandler->>RequestID: Pass Request
RequestID->>Logging: Attach Request ID
Logging->>Auth: Log Request Start
Auth->>RateLimit: Validate API Key & Role
RateLimit->>ContentType: Check Rate Limit
ContentType->>Handler: Validate Content-Type
Handler->>Storage: CRUD Operation
Storage-->>Handler: Return Result
Handler-->>Client: JSON Response + Headers
Note over Client: Rate Limit & Request ID Headers Included
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
packages/gin/challenge-2-middleware/submissions/shansing/solution.go (1)
258-264: Preferstrings.HasPrefixfor content-type check.Using
strings.HasPrefixis more idiomatic and readable than checkingstrings.Index(...) != 0.♻️ Proposed refactor
return func(c *gin.Context) { // Check content type for POST/PUT requests if c.Request.Method == "POST" || c.Request.Method == "PUT" { // Must be application/json - if strings.Index(c.ContentType(), "application/json") != 0 { + if !strings.HasPrefix(c.ContentType(), "application/json") { // Return 415 if invalid content type c.AbortWithStatus(http.StatusUnsupportedMediaType) return } }
| func responseSuccess(c *gin.Context, data interface{}) APIResponse { | ||
| requestId, _ := c.Get(requestIdKey) | ||
| return APIResponse{ | ||
| Success: true, | ||
| Data: data, | ||
| Message: "Success", | ||
| RequestID: requestId.(string), | ||
| } | ||
| } |
There was a problem hiding this comment.
Unsafe type assertion may panic if request ID is not set.
If c.Get(requestIdKey) returns nil (e.g., middleware order changes or early panic), the type assertion requestId.(string) will panic. Use the comma-ok idiom or c.GetString().
🛡️ Proposed fix using GetString
func responseSuccess(c *gin.Context, data interface{}) APIResponse {
- requestId, _ := c.Get(requestIdKey)
+ requestId := c.GetString(requestIdKey)
return APIResponse{
Success: true,
Data: data,
Message: "Success",
- RequestID: requestId.(string),
+ RequestID: requestId,
}
}
func responseFailure(c *gin.Context, err string) APIResponse {
- requestId, _ := c.Get(requestIdKey)
+ requestId := c.GetString(requestIdKey)
return APIResponse{
Success: false,
Error: err,
- RequestID: requestId.(string),
+ RequestID: requestId,
}
}
func responseFailureMessage(c *gin.Context, err string, msg string) APIResponse {
- requestId, _ := c.Get(requestIdKey)
+ requestId := c.GetString(requestIdKey)
return APIResponse{
Success: false,
Error: err,
Message: msg,
- RequestID: requestId.(string),
+ RequestID: requestId,
}
}📝 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.
| func responseSuccess(c *gin.Context, data interface{}) APIResponse { | |
| requestId, _ := c.Get(requestIdKey) | |
| return APIResponse{ | |
| Success: true, | |
| Data: data, | |
| Message: "Success", | |
| RequestID: requestId.(string), | |
| } | |
| } | |
| func responseSuccess(c *gin.Context, data interface{}) APIResponse { | |
| requestId := c.GetString(requestIdKey) | |
| return APIResponse{ | |
| Success: true, | |
| Data: data, | |
| Message: "Success", | |
| RequestID: requestId, | |
| } | |
| } |
| // RateLimitMiddleware implements rate limiting per IP | ||
| func RateLimitMiddleware() gin.HandlerFunc { | ||
| // Implement rate limiting | ||
| // Limit: 100 requests per IP per minute | ||
| requestsPerIpPerMinute := 100 | ||
| ipAndLimiterMap := new(sync.Map) | ||
|
|
||
| return func(c *gin.Context) { | ||
| // Use golang.org/x/time/rate package | ||
| // this is not "exactly" per minute, but let's do the simple way... | ||
| l, _ := ipAndLimiterMap.LoadOrStore(c.ClientIP(), | ||
| //rate.NewLimiter(rate.Every(time.Minute), requestsPerIpPerMinute), | ||
| rate.NewLimiter(rate.Limit((requestsPerIpPerMinute)/60.0), requestsPerIpPerMinute), | ||
| ) | ||
| limiter := l.(*rate.Limiter) | ||
| // Set headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | ||
| c.Header("X-RateLimit-Limit", fmt.Sprint(requestsPerIpPerMinute)) | ||
| remain := math.Floor(limiter.Tokens() - 1) | ||
| if remain < 0 { | ||
| remain = 0 | ||
| } | ||
| c.Header("X-RateLimit-Remaining", fmt.Sprint(remain)) | ||
| // I don't see how I can implement the header with golang.org/x/time/rate... | ||
| c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Unix()+120, 10)) | ||
| // Return 429 if rate limit exceeded | ||
| if !limiter.Allow() { | ||
| c.AbortWithStatus(http.StatusTooManyRequests) | ||
| return | ||
| } | ||
| c.Next() | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential memory leak: rate limiters are never evicted.
The ipAndLimiterMap grows unbounded as new client IPs are seen. For a long-running server with many unique IPs, this will consume increasing memory. Consider adding a TTL-based eviction mechanism or using a bounded cache.
Would you like me to suggest an implementation using a time-based cleanup goroutine or a bounded LRU cache?
| idString := c.Param("id") | ||
| id, err := strconv.Atoi(idString) | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | ||
| } |
There was a problem hiding this comment.
Critical: Missing return after error response.
If strconv.Atoi fails, the function continues execution with id = 0 (default int value). This will attempt to find an article with ID 0, leading to incorrect behavior.
🐛 Proposed fix
id, err := strconv.Atoi(idString)
if err != nil {
c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error()))
+ return
}📝 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.
| idString := c.Param("id") | |
| id, err := strconv.Atoi(idString) | |
| if err != nil { | |
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | |
| } | |
| idString := c.Param("id") | |
| id, err := strconv.Atoi(idString) | |
| if err != nil { | |
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | |
| return | |
| } |
| idString := c.Param("id") | ||
| id, err := strconv.Atoi(idString) | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | ||
| } |
There was a problem hiding this comment.
Critical: Missing return after error response.
Same issue as in getArticle - the function continues with id = 0 after a parsing error.
🐛 Proposed fix
id, err := strconv.Atoi(idString)
if err != nil {
c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error()))
+ return
}📝 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.
| idString := c.Param("id") | |
| id, err := strconv.Atoi(idString) | |
| if err != nil { | |
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | |
| } | |
| idString := c.Param("id") | |
| id, err := strconv.Atoi(idString) | |
| if err != nil { | |
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | |
| return | |
| } |
| article.Title = articleParam.Title | ||
| article.Content = articleParam.Content | ||
| article.Author = articleParam.Author | ||
| // Return updated article | ||
| c.JSON(http.StatusOK, responseSuccess(c, article)) |
There was a problem hiding this comment.
UpdatedAt timestamp is not being set.
When updating an article, the UpdatedAt field should reflect the modification time. Currently, it retains the original value.
🐛 Proposed fix
article.Title = articleParam.Title
article.Content = articleParam.Content
article.Author = articleParam.Author
+ article.UpdatedAt = time.Now()
// Return updated article
c.JSON(http.StatusOK, responseSuccess(c, article))📝 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.
| article.Title = articleParam.Title | |
| article.Content = articleParam.Content | |
| article.Author = articleParam.Author | |
| // Return updated article | |
| c.JSON(http.StatusOK, responseSuccess(c, article)) | |
| article.Title = articleParam.Title | |
| article.Content = articleParam.Content | |
| article.Author = articleParam.Author | |
| article.UpdatedAt = time.Now() | |
| // Return updated article | |
| c.JSON(http.StatusOK, responseSuccess(c, article)) |
| idString := c.Param("id") | ||
| id, err := strconv.Atoi(idString) | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | ||
| } |
There was a problem hiding this comment.
Critical: Missing return after error response.
Same issue as in getArticle and updateArticle - the function continues with id = 0 after a parsing error.
🐛 Proposed fix
id, err := strconv.Atoi(idString)
if err != nil {
c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error()))
+ return
}📝 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.
| idString := c.Param("id") | |
| id, err := strconv.Atoi(idString) | |
| if err != nil { | |
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | |
| } | |
| idString := c.Param("id") | |
| id, err := strconv.Atoi(idString) | |
| if err != nil { | |
| c.JSON(http.StatusBadRequest, responseFailure(c, "invalid id: "+err.Error())) | |
| return | |
| } |
| stats := map[string]interface{}{ | ||
| "total_articles": len(articles), | ||
| "total_requests": 0, // Could track this in middleware | ||
| "uptime": "24h", | ||
| } |
There was a problem hiding this comment.
Data race: accessing articles slice without holding the lock.
len(articles) is read without acquiring articlesLock, while other handlers modify articles under the lock. This is a data race.
🐛 Proposed fix
func getStats(c *gin.Context) {
// Check if user role is "admin"
role, ok := c.Get("role")
if !ok || role.(string) != "admin" {
c.AbortWithStatus(http.StatusForbidden)
return
}
+ articlesLock.RLock()
+ totalArticles := len(articles)
+ articlesLock.RUnlock()
// Return mock statistics
stats := map[string]interface{}{
- "total_articles": len(articles),
+ "total_articles": totalArticles,
"total_requests": 0, // Could track this in middleware
"uptime": "24h",
}📝 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.
| stats := map[string]interface{}{ | |
| "total_articles": len(articles), | |
| "total_requests": 0, // Could track this in middleware | |
| "uptime": "24h", | |
| } | |
| articlesLock.RLock() | |
| totalArticles := len(articles) | |
| articlesLock.RUnlock() | |
| // Return mock statistics | |
| stats := map[string]interface{}{ | |
| "total_articles": totalArticles, | |
| "total_requests": 0, // Could track this in middleware | |
| "uptime": "24h", | |
| } |
gin challenge-2-middleware Solution
Submitted by: @shansing
Package: gin
Challenge: challenge-2-middleware
Description
This PR contains my solution for gin challenge-2-middleware.
Changes
packages/gin/challenge-2-middleware/submissions/shansing/solution.goTesting
Thank you for reviewing my submission! 🚀