66 "fmt"
77 "io"
88 "net/http"
9- "reflect"
109 "regexp"
1110 "strings"
1211
@@ -18,7 +17,6 @@ type VersionLocation string
1817
1918const (
2019 VersionLocationHeader VersionLocation = "header"
21- VersionLocationQuery VersionLocation = "query"
2220 VersionLocationPath VersionLocation = "path"
2321)
2422
@@ -56,23 +54,6 @@ func (hvm *HeaderVersionManager) GetVersion(c *gin.Context) (string, error) {
5654 return version , nil
5755}
5856
59- // QueryVersionManager extracts version from query parameters
60- type QueryVersionManager struct {
61- paramName string
62- }
63-
64- // NewQueryVersionManager creates a new query-based version manager
65- func NewQueryVersionManager (paramName string ) * QueryVersionManager {
66- return & QueryVersionManager {
67- paramName : paramName ,
68- }
69- }
70-
71- func (qvm * QueryVersionManager ) GetVersion (c * gin.Context ) (string , error ) {
72- version := c .Query (qvm .paramName )
73- return version , nil
74- }
75-
7657// PathVersionManager extracts version from URL path
7758type PathVersionManager struct {
7859 versionRegex * regexp.Regexp
@@ -135,18 +116,17 @@ func NewVersionMiddleware(config MiddlewareConfig) *VersionMiddleware {
135116 var versionManager VersionManager
136117
137118 switch config .Location {
138- case VersionLocationHeader :
139- versionManager = NewHeaderVersionManager (config .ParameterName )
140- case VersionLocationQuery :
141- versionManager = NewQueryVersionManager (config .ParameterName )
142119 case VersionLocationPath :
143120 versions := make ([]string , len (config .VersionBundle .GetVersions ()))
144121 for i , v := range config .VersionBundle .GetVersions () {
145122 versions [i ] = v .String ()
146123 }
147124 versionManager = NewPathVersionManager (versions )
125+ case VersionLocationHeader :
126+ fallthrough
148127 default :
149- versionManager = NewHeaderVersionManager ("X-API-Version" )
128+ // Default to header-based versioning
129+ versionManager = NewHeaderVersionManager (config .ParameterName )
150130 }
151131
152132 return & VersionMiddleware {
@@ -194,10 +174,16 @@ func (vm *VersionMiddleware) Middleware() gin.HandlerFunc {
194174 requestedVersion = vm .findClosestOlderVersion (versionStr )
195175 }
196176 if requestedVersion == nil {
177+ var hint string
178+ if vm .location == VersionLocationHeader {
179+ hint = fmt .Sprintf ("Use '%s' header with one of the available versions" , vm .parameterName )
180+ } else {
181+ hint = "Include version in the URL path (e.g., /v1/resource or /2024-01-01/resource)"
182+ }
197183 c .JSON (http .StatusBadRequest , gin.H {
198184 "error" : fmt .Sprintf ("Unknown version: %s" , versionStr ),
199185 "available_versions" : vm .versionBundle .GetVersionValues (),
200- "hint" : fmt . Sprintf ( "Use %s header with one of the available versions" , vm . parameterName ) ,
186+ "hint" : hint ,
201187 })
202188 c .Abort ()
203189 return
@@ -369,11 +355,24 @@ func (vah *VersionAwareHandler) migrateRequest(c *gin.Context, fromVersion *Vers
369355 return nil // No body to migrate
370356 }
371357
372- // Parse JSON body directly - ShouldBindJSON handles reading and parsing
358+ // Read body once and preserve it
359+ bodyBytes , err := io .ReadAll (c .Request .Body )
360+ if err != nil {
361+ return fmt .Errorf ("failed to read request body: %w" , err )
362+ }
363+ c .Request .Body .Close ()
364+
365+ // If body is empty, nothing to migrate
366+ if len (bodyBytes ) == 0 {
367+ c .Request .Body = io .NopCloser (bytes .NewReader (bodyBytes ))
368+ return nil
369+ }
370+
371+ // Parse JSON body
373372 var bodyData interface {}
374- if err := c . ShouldBindJSON ( & bodyData ); err != nil {
375- // If JSON parsing fails, the body is already consumed
376- // We can't restore it, so just return (handler will also fail to parse )
373+ if err := json . Unmarshal ( bodyBytes , & bodyData ); err != nil {
374+ // If JSON parsing fails, restore original body and let handler deal with it
375+ c . Request . Body = io . NopCloser ( bytes . NewReader ( bodyBytes ) )
377376 return nil
378377 }
379378
@@ -385,8 +384,10 @@ func (vah *VersionAwareHandler) migrateRequest(c *gin.Context, fromVersion *Vers
385384 migrationChain := vah .migrationChain .GetMigrationPath (fromVersion , headVersion )
386385
387386 // Apply migrations in forward direction
387+ // Note: We pass nil for bodyType since JSON unmarshaling creates map[string]interface{}
388+ // which doesn't match the original struct types. Schema-based matching doesn't work with JSON.
388389 for _ , change := range migrationChain {
389- if err := change .MigrateRequest (c .Request .Context (), requestInfo , reflect . TypeOf ( requestInfo . Body ) , 0 ); err != nil {
390+ if err := change .MigrateRequest (c .Request .Context (), requestInfo , nil , 0 ); err != nil {
390391 return fmt .Errorf ("failed to migrate request with change %s: %w" , change .Description (), err )
391392 }
392393 }
@@ -427,19 +428,22 @@ func (vah *VersionAwareHandler) migrateResponse(c *gin.Context, toVersion *Versi
427428 migrationChain := vah .migrationChain .GetMigrationPath (headVersion , toVersion )
428429
429430 // Apply migrations in reverse direction
431+ // Note: We pass nil for responseType since JSON unmarshaling creates map[string]interface{}
432+ // which doesn't match the original struct types. Schema-based matching doesn't work with JSON.
430433 for i := len (migrationChain ) - 1 ; i >= 0 ; i -- {
431434 change := migrationChain [i ]
432- if err := change .MigrateResponse (c .Request .Context (), responseInfo , reflect . TypeOf ( responseInfo . Body ) , 0 ); err != nil {
435+ if err := change .MigrateResponse (c .Request .Context (), responseInfo , nil , 0 ); err != nil {
433436 return fmt .Errorf ("failed to migrate response with change %s: %w" , change .Description (), err )
434437 }
435438 }
436439
437440 // Write the migrated response
438441 c .Writer = responseCapture .ResponseWriter
439- c .Writer .WriteHeader (responseInfo .StatusCode )
440442
441443 if responseInfo .Body != nil {
442444 c .JSON (responseInfo .StatusCode , responseInfo .Body )
445+ } else {
446+ c .Writer .WriteHeader (responseInfo .StatusCode )
443447 }
444448
445449 return nil
0 commit comments