@sun-asterisk/sunlint 1.3.47 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Go Gin Framework — SunLint Agent Guide
|
|
2
|
+
|
|
3
|
+
> Priority directives for AI agents working on Go + Gin projects.
|
|
4
|
+
> Rule files: `.agent/skills/sunlint-code-quality/rules/`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Critical Patterns — Apply Every Time
|
|
9
|
+
|
|
10
|
+
### Middleware Chain Termination
|
|
11
|
+
- After sending a response that should stop the chain → `c.AbortWithStatusJSON(code, body)` then `return`
|
|
12
|
+
- `c.AbortWithStatusJSON` calls `c.Abort()` internally — don't call both
|
|
13
|
+
- Plain `return` from a handler does NOT stop downstream handlers
|
|
14
|
+
- See: `GN001-abort-after-response.md`
|
|
15
|
+
|
|
16
|
+
### Request Binding + Validation
|
|
17
|
+
- Every `c.ShouldBindJSON(&req)` → must be followed by `if err != nil { c.AbortWithStatusJSON(400, ...); return }`
|
|
18
|
+
- Declare all validation rules as struct `binding:` tags: `binding:"required,email"`, `binding:"min=1,max=255"`
|
|
19
|
+
- **Never** skip the error check from `ShouldBind*`
|
|
20
|
+
- See: `GN003-bind-error-handling.md`, `GN008-struct-validation-tags.md`
|
|
21
|
+
|
|
22
|
+
### Context Usage
|
|
23
|
+
- Inside handlers: `ctx := c.Request.Context()` — pass to all downstream calls
|
|
24
|
+
- **Never** `context.Background()` inside a handler
|
|
25
|
+
- **Never** pass `*gin.Context` to a goroutine — copy scalar values first, pass `c.Request.Context()`
|
|
26
|
+
- See: `GN002-request-context.md`, `GN010-context-scope.md`
|
|
27
|
+
|
|
28
|
+
### Do Not Log Sensitive DataTesting commands rõ ràng trong package.json/Makefile
|
|
29
|
+
Standard conventions (eslint, prettier, gofmt đã config sẵn)
|
|
30
|
+
4. Test với agents trước khi commit
|
|
31
|
+
Quy trình recommended:
|
|
32
|
+
|
|
33
|
+
Bước 1: Tạo baseline
|
|
34
|
+
|
|
35
|
+
# Chạy agent KHÔNG có AGENTS.md trên vài tasks nhỏ
|
|
36
|
+
# Đo success rate, cost
|
|
37
|
+

|
|
38
|
+
Bước 2: Thêm AGENTS.md, measure lại
|
|
39
|
+
|
|
40
|
+
# So sánh với baseline
|
|
41
|
+
# Nếu không improve hoặc cost tăng quá nhiều → bỏ AGENTS.md
|
|
42
|
+

|
|
43
|
+
Bước 3: Iterate
|
|
44
|
+
|
|
45
|
+
# Nếu quyết định giữ AGENTS.md, hãy giữ nó concise
|
|
46
|
+
# Monitor agent behavior qua time
|
|
47
|
+

|
|
48
|
+
5. Monitor Context Length
|
|
49
|
+
Context files nên dưới 500 tokens (t
|
|
50
|
+
- Logging middleware logs only: method, path, status, duration, request_id, client IP
|
|
51
|
+
- **Never** log request bodies, `Authorization` header values, or any field named password/token/card
|
|
52
|
+
- See: `GN012-no-log-sensitive.md`
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Architecture Patterns
|
|
57
|
+
|
|
58
|
+
### Handler struct — dependency injection
|
|
59
|
+
Handlers are structs with injected service interfaces. No globals. No `new(Service)` inside handlers.
|
|
60
|
+
```go
|
|
61
|
+
type OrderHandler struct {
|
|
62
|
+
service OrderService // interface — mockable
|
|
63
|
+
logger *slog.Logger
|
|
64
|
+
}
|
|
65
|
+
func NewOrderHandler(svc OrderService, logger *slog.Logger) *OrderHandler {
|
|
66
|
+
return &OrderHandler{service: svc, logger: logger}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
See: `GN004-dependency-injection.md`
|
|
70
|
+
|
|
71
|
+
### Route organisation
|
|
72
|
+
```go
|
|
73
|
+
r := gin.New()
|
|
74
|
+
r.Use(gin.Recovery(), RequestID(), RequestLogger(logger)) // global
|
|
75
|
+
|
|
76
|
+
public := r.Group("/api/v1")
|
|
77
|
+
{ /* unauthenticated routes */ }
|
|
78
|
+
|
|
79
|
+
private := r.Group("/api/v1")
|
|
80
|
+
private.Use(JWTAuth(validator))
|
|
81
|
+
{ /* authenticated routes */ }
|
|
82
|
+
```
|
|
83
|
+
See: `GN005-route-groups-middleware.md`, `GN011-middleware-concerns.md`
|
|
84
|
+
|
|
85
|
+
### Production setup checklist
|
|
86
|
+
```go
|
|
87
|
+
gin.SetMode(os.Getenv("GIN_MODE")) // "release" in prod — GN007
|
|
88
|
+
r := gin.New()
|
|
89
|
+
r.Use(CustomRecovery(logger)) // GN009
|
|
90
|
+
r.Use(RequestID()) // GN011
|
|
91
|
+
r.Use(RequestLogger(logger)) // GN012
|
|
92
|
+
// ... register groups (GN005)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### HTTP Status Codes
|
|
96
|
+
- `POST` success → `c.JSON(http.StatusCreated, obj)` (201)
|
|
97
|
+
- `DELETE` no body → `c.Status(http.StatusNoContent)` (204)
|
|
98
|
+
- Not found → `c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})` (404)
|
|
99
|
+
- Validation error → `c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})` (400)
|
|
100
|
+
- **Always** use `net/http` constants — never magic integers
|
|
101
|
+
- See: `GN006-http-status-codes.md`
|
|
102
|
+
|
|
103
|
+
### Cross-cutting concerns
|
|
104
|
+
Auth, rate limiting, CORS, request ID, structured logging → **always middleware**, never inside handlers.
|
|
105
|
+
See: `GN011-middleware-concerns.md`
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Run Commands
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Development
|
|
113
|
+
go run ./cmd/api/main.go
|
|
114
|
+
GIN_MODE=debug go run ./cmd/api/main.go
|
|
115
|
+
|
|
116
|
+
# Build
|
|
117
|
+
go build -o bin/api ./cmd/api/
|
|
118
|
+
|
|
119
|
+
# Test (always with race detector)
|
|
120
|
+
go test -race ./...
|
|
121
|
+
go test -race -run TestOrderHandler ./internal/handlers/
|
|
122
|
+
go test -cover ./...
|
|
123
|
+
|
|
124
|
+
# Linting
|
|
125
|
+
golangci-lint run ./...
|
|
126
|
+
staticcheck ./...
|
|
127
|
+
|
|
128
|
+
# Env for production
|
|
129
|
+
GIN_MODE=release
|
|
130
|
+
PORT=8080
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## What NOT to do (quick reference)
|
|
136
|
+
|
|
137
|
+
| ❌ Wrong | ✅ Correct |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `return` after `c.JSON` in middleware | `c.AbortWithStatusJSON(...)` then `return` |
|
|
140
|
+
| `context.Background()` in handler | `c.Request.Context()` |
|
|
141
|
+
| `go func() { use c.Get(...) }()` | Copy value before goroutine: `val := c.GetString(...)` |
|
|
142
|
+
| `c.ShouldBindJSON(&req)` without err check | `if err := c.ShouldBindJSON(&req); err != nil { abort }` |
|
|
143
|
+
| Manual `if req.Email == ""` validation | `binding:"required,email"` struct tag |
|
|
144
|
+
| `var db *gorm.DB` package global | Inject `db *gorm.DB` as struct field |
|
|
145
|
+
| Auth check inside handler | `JWTAuth()` middleware on route group |
|
|
146
|
+
| `log.Printf("body: %s", body)` | Log method+path+status+duration only |
|
|
147
|
+
| `gin.Default()` in production | `gin.New()` + explicit `Recovery()` + structured logger |
|
|
148
|
+
| `c.JSON(200, gin.H{"error": ...})` | `c.AbortWithStatusJSON(http.StatusBadRequest, ...)` |
|
|
149
|
+
| `GIN_MODE` not set (debug default) | `GIN_MODE=release` in production environment |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN001 – Call c.Abort() After Terminating in Middleware"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Missing c.Abort() causes subsequent middleware handlers and the final route handler to still execute after an early response has been sent, causing duplicate writes and data leaks."
|
|
5
|
+
tags: [go, gin, middleware, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN001 – Call `c.Abort()` After Terminating in Middleware
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Any middleware that sends a response and intends to stop the chain **must** call `c.Abort()` (or `c.AbortWithStatus()` / `c.AbortWithStatusJSON()`) immediately after writing. Never rely on `return` alone.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`c.Next()` executes subsequent handlers in sequence. A plain `return` exits the current handler but Gin still runs the remaining handlers in the chain. `c.Abort()` sets the index past all remaining handlers so the chain stops cleanly.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func AuthMiddleware() gin.HandlerFunc {
|
|
22
|
+
return func(c *gin.Context) {
|
|
23
|
+
token := c.GetHeader("Authorization")
|
|
24
|
+
if token == "" {
|
|
25
|
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
26
|
+
return // ❌ returns from this func but downstream handlers still run
|
|
27
|
+
}
|
|
28
|
+
c.Next()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func RateLimitMiddleware() gin.HandlerFunc {
|
|
33
|
+
return func(c *gin.Context) {
|
|
34
|
+
if isRateLimited(c.ClientIP()) {
|
|
35
|
+
c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
|
|
36
|
+
// ❌ forgot both return and Abort — next handler fires and writes again
|
|
37
|
+
}
|
|
38
|
+
c.Next()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Correct
|
|
44
|
+
|
|
45
|
+
```go
|
|
46
|
+
func AuthMiddleware() gin.HandlerFunc {
|
|
47
|
+
return func(c *gin.Context) {
|
|
48
|
+
token := c.GetHeader("Authorization")
|
|
49
|
+
if token == "" {
|
|
50
|
+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
51
|
+
// c.Abort() is called inside AbortWithStatusJSON — no c.Next() needed
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
// validation passes — continue chain
|
|
55
|
+
c.Set("user_id", parsedUserID)
|
|
56
|
+
c.Next()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func RateLimitMiddleware() gin.HandlerFunc {
|
|
61
|
+
return func(c *gin.Context) {
|
|
62
|
+
if isRateLimited(c.ClientIP()) {
|
|
63
|
+
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
c.Next()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- `c.AbortWithStatus(code)` and `c.AbortWithStatusJSON(code, obj)` both call `c.Abort()` internally — you don't need to call `c.Abort()` separately when using these.
|
|
74
|
+
- After `c.Abort()`, code after the call in the same function still runs — use `return` to exit the function body.
|
|
75
|
+
- Verify with unit tests: register middleware + handler, send a bad request, assert the handler body was NOT executed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN002 – Use c.Request.Context(), Not context.Background()"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "context.Background() ignores client disconnects and request deadlines; c.Request.Context() propagates cancellation automatically."
|
|
5
|
+
tags: [go, gin, context, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN002 – Use `c.Request.Context()` Not `context.Background()`
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
When passing a context to database calls, external HTTP requests, or any `ctx context.Context` parameter inside a Gin handler, always use `c.Request.Context()`. Never create a fresh `context.Background()` inside a handler.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`c.Request.Context()` is derived from the HTTP request and carries the client's cancellation signal. If the client disconnects mid-request, the context is cancelled and all downstream work (DB queries, gRPC calls) is cancelled too — preventing wasted compute. `context.Background()` is never cancelled and leaks work after the client is gone.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *OrderHandler) Create(c *gin.Context) {
|
|
22
|
+
var req CreateOrderRequest
|
|
23
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
24
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ❌ context.Background() — ignores client disconnect and server deadlines
|
|
29
|
+
order, err := h.service.CreateOrder(context.Background(), req)
|
|
30
|
+
if err != nil {
|
|
31
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
c.JSON(http.StatusCreated, order)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Correct
|
|
39
|
+
|
|
40
|
+
```go
|
|
41
|
+
func (h *OrderHandler) Create(c *gin.Context) {
|
|
42
|
+
var req CreateOrderRequest
|
|
43
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
44
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ propagates request lifetime, deadline, and cancellation
|
|
49
|
+
ctx := c.Request.Context()
|
|
50
|
+
order, err := h.service.CreateOrder(ctx, req)
|
|
51
|
+
if err != nil {
|
|
52
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
c.JSON(http.StatusCreated, order)
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Extract the context once: `ctx := c.Request.Context()` at the top of the handler body.
|
|
62
|
+
- Do **not** store this context in a struct field — contexts must flow through function arguments.
|
|
63
|
+
- If you need a timeout shorter than the request's own deadline: `ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second); defer cancel()`.
|
|
64
|
+
- See GN010 for the rule about not storing Gin context in goroutines.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN003 – Always Handle ShouldBindJSON / ShouldBind Errors"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Ignoring ShouldBindJSON errors means malformed or missing fields are silently treated as zero values, causing corrupted data writes and bypassed validation."
|
|
5
|
+
tags: [go, gin, validation, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN003 – Always Handle `ShouldBindJSON` / `ShouldBind` Errors
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Every call to `c.ShouldBindJSON`, `c.ShouldBind`, `c.ShouldBindQuery`, or `c.ShouldBindUri` must be followed by an error check. If the error is non-nil, abort immediately with a `400 Bad Request` response.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
`ShouldBind*` populates the target struct but returns an error for malformed JSON, missing required fields (enforced by `binding:"required"` tags), or type mismatches. Ignoring the error means the handler continues with a partially or incorrectly populated struct, silently writing bad data.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
22
|
+
var req CreateUserRequest
|
|
23
|
+
c.ShouldBindJSON(&req) // ❌ error ignored — continues with zero-value struct fields
|
|
24
|
+
|
|
25
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
26
|
+
// ...
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Worse: using BindJSON (panics on error in some versions, logs but doesn't stop)
|
|
30
|
+
func (h *UserHandler) Update(c *gin.Context) {
|
|
31
|
+
var req UpdateUserRequest
|
|
32
|
+
c.BindJSON(&req) // ❌ BindJSON writes 400 header but handler still continues
|
|
33
|
+
h.service.UpdateUser(c.Request.Context(), req)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Correct
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
type CreateUserRequest struct {
|
|
41
|
+
Name string `json:"name" binding:"required,min=2,max=100"`
|
|
42
|
+
Email string `json:"email" binding:"required,email"`
|
|
43
|
+
Age int `json:"age" binding:"required,min=18"`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
47
|
+
var req CreateUserRequest
|
|
48
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
49
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
50
|
+
"error": "invalid request",
|
|
51
|
+
"details": err.Error(),
|
|
52
|
+
})
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
57
|
+
if err != nil {
|
|
58
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"})
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
c.JSON(http.StatusCreated, user)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Notes
|
|
66
|
+
|
|
67
|
+
- Prefer `ShouldBind*` over `Bind*` — the `Bind*` family writes a `400` header but doesn't abort the handler, making code flow confusing.
|
|
68
|
+
- Use struct tags: `binding:"required"`, `binding:"email"`, `binding:"min=1,max=255"` to shift validation into the binding layer.
|
|
69
|
+
- For detailed error messages, type-assert the error to `validator.ValidationErrors` and format field names cleanly.
|
|
70
|
+
- See GN008 for struct validation tag conventions.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN004 – Use Dependency Injection, Not Global Variables"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Global database connections and service instances in package-level vars are untestable, race-prone, and make initialization order fragile."
|
|
5
|
+
tags: [go, gin, architecture, testing]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN004 – Use Dependency Injection, Not Global Variables
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Database connections, service instances, configuration objects, and HTTP clients must be injected as struct fields into handlers. Never declare them as `var db *gorm.DB` at package level and access them from handler functions.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Global state:
|
|
17
|
+
- Cannot be replaced with a mock in unit tests.
|
|
18
|
+
- Initialization order is implicit — the global may be `nil` if setup hasn't run.
|
|
19
|
+
- Race conditions when tests share globals.
|
|
20
|
+
|
|
21
|
+
Dependency injection via struct fields makes dependencies explicit, swappable, and testable.
|
|
22
|
+
|
|
23
|
+
## Wrong
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
// db/db.go — package-level global
|
|
27
|
+
var DB *gorm.DB
|
|
28
|
+
|
|
29
|
+
func Init() {
|
|
30
|
+
DB, _ = gorm.Open(...)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// handlers/user.go — accesses global directly
|
|
34
|
+
func GetUser(c *gin.Context) {
|
|
35
|
+
var user User
|
|
36
|
+
db.DB.First(&user, c.Param("id")) // ❌ depends on global initialization order
|
|
37
|
+
c.JSON(200, user)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Correct
|
|
42
|
+
|
|
43
|
+
```go
|
|
44
|
+
// handlers/user_handler.go
|
|
45
|
+
type UserHandler struct {
|
|
46
|
+
db *gorm.DB // injected
|
|
47
|
+
service UserService // interface — swappable in tests
|
|
48
|
+
logger *slog.Logger
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func NewUserHandler(db *gorm.DB, svc UserService, logger *slog.Logger) *UserHandler {
|
|
52
|
+
return &UserHandler{db: db, service: svc, logger: logger}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (h *UserHandler) GetUser(c *gin.Context) {
|
|
56
|
+
ctx := c.Request.Context()
|
|
57
|
+
user, err := h.service.FindUserByID(ctx, c.Param("id"))
|
|
58
|
+
if err != nil {
|
|
59
|
+
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
c.JSON(http.StatusOK, user)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// main.go — wiring
|
|
66
|
+
db := database.Connect(cfg.DSN)
|
|
67
|
+
userSvc := services.NewUserService(db)
|
|
68
|
+
userHandler := handlers.NewUserHandler(db, userSvc, logger)
|
|
69
|
+
|
|
70
|
+
r := gin.New()
|
|
71
|
+
r.GET("/users/:id", userHandler.GetUser)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- Define service dependencies as Go interfaces so handlers can be unit-tested with mocks without a real DB.
|
|
77
|
+
- Use a DI framework (e.g., `google/wire`, `samber/do`) for large projects to automate wiring.
|
|
78
|
+
- Configuration (`Config` structs) should be loaded once in `main` and injected — never read from `os.Getenv` directly inside handlers.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN005 – Group Routes and Apply Middleware at Group Level"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Registering middleware per-route duplicates code and makes it easy to miss a route; group-level middleware is exhaustive by design."
|
|
5
|
+
tags: [go, gin, routing, middleware]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN005 – Group Routes and Apply Middleware at Group Level
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Use `router.Group()` to organise routes by prefix and protection level. Apply authentication, logging, and rate-limiting middleware to the group — not individual routes.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Per-route middleware registration (`r.GET("/users", authMiddleware, handler)`) requires the developer to remember to add middleware to every new route. A missed route is a security hole. Group-level middleware is applied to all current and future routes in the group automatically.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
r := gin.Default()
|
|
22
|
+
|
|
23
|
+
// ❌ Auth repeated on every route — easy to miss
|
|
24
|
+
r.GET("/users", authMiddleware, userHandler.List)
|
|
25
|
+
r.POST("/users", authMiddleware, userHandler.Create)
|
|
26
|
+
r.PUT("/users/:id", authMiddleware, userHandler.Update)
|
|
27
|
+
r.DELETE("/users/:id", authMiddleware, userHandler.Delete) // ❌ forgot auth on DELETE
|
|
28
|
+
|
|
29
|
+
// ❌ Mixed public and protected routes without groups — hard to audit
|
|
30
|
+
r.GET("/products", productHandler.List)
|
|
31
|
+
r.POST("/products", authMiddleware, adminMiddleware, productHandler.Create)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Correct
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
r := gin.New()
|
|
38
|
+
r.Use(gin.Recovery(), requestLogger()) // global middleware
|
|
39
|
+
|
|
40
|
+
// Public routes — no auth
|
|
41
|
+
public := r.Group("/api/v1")
|
|
42
|
+
{
|
|
43
|
+
public.POST("/auth/login", authHandler.Login)
|
|
44
|
+
public.POST("/auth/register", authHandler.Register)
|
|
45
|
+
public.GET("/products", productHandler.List)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Authenticated routes
|
|
49
|
+
authenticated := r.Group("/api/v1")
|
|
50
|
+
authenticated.Use(authMiddleware())
|
|
51
|
+
{
|
|
52
|
+
authenticated.GET("/users/me", userHandler.Profile)
|
|
53
|
+
authenticated.PUT("/users/me", userHandler.Update)
|
|
54
|
+
authenticated.GET("/orders", orderHandler.List)
|
|
55
|
+
authenticated.POST("/orders", orderHandler.Create)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Admin-only routes
|
|
59
|
+
admin := r.Group("/api/v1/admin")
|
|
60
|
+
admin.Use(authMiddleware(), adminMiddleware())
|
|
61
|
+
{
|
|
62
|
+
admin.GET("/users", adminUserHandler.List)
|
|
63
|
+
admin.DELETE("/users/:id", adminUserHandler.Delete)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Notes
|
|
68
|
+
|
|
69
|
+
- Use curly braces `{}` inside `Group()` to make the scope visually clear (they're just Go blocks, not syntactically required).
|
|
70
|
+
- Apply rate limiting at the group level for public endpoints to prevent brute force on `/auth/login`.
|
|
71
|
+
- Version your API with a group: `r.Group("/api/v1")` makes future versioning (`/api/v2`) straightforward.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN006 – Return Correct HTTP Status Codes"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Returning 200 for all responses breaks API clients, hides errors in monitoring, and makes retry logic impossible."
|
|
5
|
+
tags: [go, gin, api, http]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN006 – Return Correct HTTP Status Codes
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Always pass the correct HTTP status code to `c.JSON()`, `c.AbortWithStatusJSON()`, and `c.Status()`. Never return `200 OK` for errors, resource creation, or empty responses.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
HTTP semantics are the contract between your API and its clients. A `200` with `{"error": "not found"}` is invisible to load balancers, monitoring tools, and client SDKs. Correct status codes enable automatic retry logic, circuit breakers, and accurate error dashboards.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
22
|
+
// ...
|
|
23
|
+
user, err := h.service.CreateUser(ctx, req)
|
|
24
|
+
if err != nil {
|
|
25
|
+
c.JSON(200, gin.H{"error": err.Error()}) // ❌ success code + error body
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
c.JSON(200, user) // ❌ should be 201 for newly created resource
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func (h *UserHandler) Delete(c *gin.Context) {
|
|
32
|
+
h.service.DeleteUser(ctx, id)
|
|
33
|
+
c.JSON(200, gin.H{"message": "deleted"}) // ❌ should be 204 with no body
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Correct
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
import "net/http"
|
|
41
|
+
|
|
42
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
43
|
+
var req CreateUserRequest
|
|
44
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
45
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // 400
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
49
|
+
if err != nil {
|
|
50
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"}) // 500
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
c.JSON(http.StatusCreated, user) // 201 ✅
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func (h *UserHandler) GetUser(c *gin.Context) {
|
|
57
|
+
user, err := h.service.FindUserByID(c.Request.Context(), c.Param("id"))
|
|
58
|
+
if errors.Is(err, ErrNotFound) {
|
|
59
|
+
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "user not found"}) // 404
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
c.JSON(http.StatusOK, user) // 200
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (h *UserHandler) Delete(c *gin.Context) {
|
|
66
|
+
if err := h.service.DeleteUser(c.Request.Context(), c.Param("id")); err != nil {
|
|
67
|
+
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"}) // 404
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
c.Status(http.StatusNoContent) // 204, no body ✅
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Status code reference
|
|
75
|
+
|
|
76
|
+
| Scenario | Code | Constant |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Read success | 200 | `http.StatusOK` |
|
|
79
|
+
| Create success | 201 | `http.StatusCreated` |
|
|
80
|
+
| Delete / no body | 204 | `http.StatusNoContent` |
|
|
81
|
+
| Validation failed | 400 | `http.StatusBadRequest` |
|
|
82
|
+
| Not authenticated | 401 | `http.StatusUnauthorized` |
|
|
83
|
+
| Not authorized | 403 | `http.StatusForbidden` |
|
|
84
|
+
| Not found | 404 | `http.StatusNotFound` |
|
|
85
|
+
| Business rule violation | 422 | `http.StatusUnprocessableEntity` |
|
|
86
|
+
| Rate limited | 429 | `http.StatusTooManyRequests` |
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Use `net/http` constants — never magic numbers like `c.JSON(201, ...)`.
|
|
91
|
+
- Map service-layer sentinel errors (e.g. `ErrNotFound`, `ErrConflict`) to status codes in a central error-mapping function.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN007 – Set gin.ReleaseMode in Production"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "The default gin.DebugMode prints every registered route and request detail to stdout, leaking your API surface and increasing log noise in production."
|
|
5
|
+
tags: [go, gin, security, configuration]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN007 – Set `gin.ReleaseMode` in Production
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Set `gin.SetMode(gin.ReleaseMode)` before calling `gin.New()` or `gin.Default()` in production builds. Read the mode from an environment variable so it can differ between environments.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Debug mode outputs all routes on startup and logs detailed request info. In production this:
|
|
17
|
+
- Leaks your API surface to anyone with log access.
|
|
18
|
+
- Adds unnecessary CPU cost for string formatting.
|
|
19
|
+
- Makes it harder to find meaningful log entries.
|
|
20
|
+
|
|
21
|
+
## Wrong
|
|
22
|
+
|
|
23
|
+
```go
|
|
24
|
+
// main.go — mode not set, defaults to DebugMode
|
|
25
|
+
func main() {
|
|
26
|
+
r := gin.Default() // ❌ runs in debug mode, dumps all routes to stdout
|
|
27
|
+
r.GET("/internal/admin", adminHandler) // ❌ exposed in startup log
|
|
28
|
+
r.Run(":8080")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Hardcoded mode
|
|
32
|
+
gin.SetMode(gin.DebugMode) // ❌ always debug, regardless of environment
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Correct
|
|
36
|
+
|
|
37
|
+
```go
|
|
38
|
+
func main() {
|
|
39
|
+
// Read from environment — "release", "debug", or "test"
|
|
40
|
+
ginMode := os.Getenv("GIN_MODE")
|
|
41
|
+
if ginMode == "" {
|
|
42
|
+
ginMode = gin.ReleaseMode // safe default
|
|
43
|
+
}
|
|
44
|
+
gin.SetMode(ginMode)
|
|
45
|
+
|
|
46
|
+
r := gin.New()
|
|
47
|
+
r.Use(gin.Recovery()) // see GN009 for Recovery middleware
|
|
48
|
+
// ... register routes
|
|
49
|
+
r.Run(":" + os.Getenv("PORT"))
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or rely on Gin's built-in env var support: Gin automatically reads `GIN_MODE` — set it to `release` in your container/systemd environment and `gin.SetMode` is not even needed explicitly.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Production container / systemd
|
|
57
|
+
GIN_MODE=release
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Notes
|
|
61
|
+
|
|
62
|
+
- `gin.Default()` adds `gin.Logger()` and `gin.Recovery()` automatically in debug mode — in production, use `gin.New()` and add your structured logger and Recovery explicitly (see GN009).
|
|
63
|
+
- Never call `gin.SetMode` after `gin.New()` — the mode must be set before the engine is created.
|
|
64
|
+
- In tests, set `gin.SetMode(gin.TestMode)` in `TestMain` to silence route debug output.
|