Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 92 additions & 34 deletions gateway/api_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,36 +93,38 @@

// Statuses of the request, all are false-y except StatusOk and StatusOkAndIgnore
const (
VersionNotFound RequestStatus = "Version information not found"
VersionDoesNotExist RequestStatus = "This API version does not seem to exist"
VersionWhiteListStatusNotFound RequestStatus = "WhiteListStatus for path not found"
VersionExpired RequestStatus = "Api Version has expired, please check documentation or contact administrator"
APIExpired RequestStatus = "API has expired, please check documentation or contact administrator"
EndPointNotAllowed RequestStatus = "Requested endpoint is forbidden"
StatusOkAndIgnore RequestStatus = "Everything OK, passing and not filtering"
StatusOk RequestStatus = "Everything OK, passing"
StatusCached RequestStatus = "Cached path"
StatusTransform RequestStatus = "Transformed path"
StatusTransformResponse RequestStatus = "Transformed response"
StatusTransformJQ RequestStatus = "Transformed path with JQ"
StatusTransformJQResponse RequestStatus = "Transformed response with JQ"
StatusHeaderInjected RequestStatus = "Header injected"
StatusMethodTransformed RequestStatus = "Method Transformed"
StatusHeaderInjectedResponse RequestStatus = "Header injected on response"
StatusRedirectFlowByReply RequestStatus = "Exceptional action requested, redirecting flow!"
StatusHardTimeout RequestStatus = "Hard Timeout enforced on path"
StatusCircuitBreaker RequestStatus = "Circuit breaker enforced"
StatusURLRewrite RequestStatus = "URL Rewritten"
StatusVirtualPath RequestStatus = "Virtual Endpoint"
StatusRequestSizeControlled RequestStatus = "Request Size Limited"
StatusRequestTracked RequestStatus = "Request Tracked"
StatusRequestNotTracked RequestStatus = "Request Not Tracked"
StatusValidateJSON RequestStatus = "Validate JSON"
StatusValidateRequest RequestStatus = "Validate Request"
StatusInternal RequestStatus = "Internal path"
StatusGoPlugin RequestStatus = "Go plugin"
StatusPersistGraphQL RequestStatus = "Persist GraphQL"
StatusRateLimit RequestStatus = "Rate Limited"
VersionNotFound RequestStatus = "Version information not found"
VersionDoesNotExist RequestStatus = "This API version does not seem to exist"
VersionWhiteListStatusNotFound RequestStatus = "WhiteListStatus for path not found"
VersionExpired RequestStatus = "Api Version has expired, please check documentation or contact administrator"
VersionDefaultForNotVersionedNotFound RequestStatus = "No default API version for this non-versioned API found"
VersionAmbiguousDefault RequestStatus = "Ambiguous default API version for this non-versioned API"
APIExpired RequestStatus = "API has expired, please check documentation or contact administrator"
EndPointNotAllowed RequestStatus = "Requested endpoint is forbidden"
StatusOkAndIgnore RequestStatus = "Everything OK, passing and not filtering"
StatusOk RequestStatus = "Everything OK, passing"
StatusCached RequestStatus = "Cached path"
StatusTransform RequestStatus = "Transformed path"
StatusTransformResponse RequestStatus = "Transformed response"
StatusTransformJQ RequestStatus = "Transformed path with JQ"
StatusTransformJQResponse RequestStatus = "Transformed response with JQ"
StatusHeaderInjected RequestStatus = "Header injected"
StatusMethodTransformed RequestStatus = "Method Transformed"
StatusHeaderInjectedResponse RequestStatus = "Header injected on response"
StatusRedirectFlowByReply RequestStatus = "Exceptional action requested, redirecting flow!"
StatusHardTimeout RequestStatus = "Hard Timeout enforced on path"
StatusCircuitBreaker RequestStatus = "Circuit breaker enforced"
StatusURLRewrite RequestStatus = "URL Rewritten"
StatusVirtualPath RequestStatus = "Virtual Endpoint"
StatusRequestSizeControlled RequestStatus = "Request Size Limited"
StatusRequestTracked RequestStatus = "Request Tracked"
StatusRequestNotTracked RequestStatus = "Request Not Tracked"
StatusValidateJSON RequestStatus = "Validate JSON"
StatusValidateRequest RequestStatus = "Validate Request"
StatusInternal RequestStatus = "Internal path"
StatusGoPlugin RequestStatus = "Go plugin"
StatusPersistGraphQL RequestStatus = "Persist GraphQL"
StatusRateLimit RequestStatus = "Rate Limited"
)

type EndPointCacheMeta struct {
Expand Down Expand Up @@ -1659,20 +1661,25 @@
var version apidef.VersionInfo

// try the context first
if v := ctxGetVersionInfo(r); v != nil {
version = *v
} else {
// Are we versioned?
if a.VersionData.NotVersioned {
// Get the first one in the list
for _, v := range a.VersionData.Versions {
version = v
break
ambiguous := a.CheckForAmbiguousDefaultVersions()
if ambiguous {
return nil, VersionAmbiguousDefault
}

ok := false
version, ok = a.GetSingleOrDefaultVersion()
if !ok {
return nil, VersionDefaultForNotVersionedNotFound
}
} else {
// Extract Version Info
// First checking for if default version is set
vName := a.getVersionFromRequest(r)

Check failure on line 1682 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: dependency

architecture Issue

The change to deterministically select a version for non-versioned APIs introduces a breaking change in runtime behavior. Previously, misconfigured APIs might have worked unpredictably; now they will consistently fail if an unambiguous default version cannot be found. This impacts `tyk-operator` and `tyk-portal`, which can now create invalid API definitions, and has critical operational consequences for `tyk-sink` (MDCB) during rolling upgrades.
Raw output
This change requires coordinated updates and documentation across the ecosystem. 1) A ticket must be raised for `tyk-operator` to add validation to its `ApiDefinition` controller to reject ambiguous configurations. 2) A ticket must be raised for `tyk-portal` to update its UI and backend validation to prevent users from creating such configurations. 3) The release notes for this gateway version must clearly document this change and its operational impact, especially for `tyk-sink` (MDCB) users, advising them to audit API definitions before performing rolling upgrades.
if vName == "" {
if a.VersionData.DefaultVersion == "" {
return &version, VersionNotFound
Expand Down Expand Up @@ -1701,6 +1708,57 @@
return &version, StatusOk
}

// GetSingleOrDefaultVersion determines and returns a single version or the default version if only one or a default exists.
// Returns versionInfo and a boolean indicating success or failure.
func (a *APISpec) GetSingleOrDefaultVersion() (versionInfo apidef.VersionInfo, ok bool) {
// If only one version exists, we can safely return this one
if len(a.VersionData.Versions) == 1 {
for _, v := range a.VersionData.Versions {
return v, true
}

Check notice on line 1718 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: quality

style Issue

The code iterates over a map with a single element to retrieve its value. While functionally correct in this specific case, relying on map iteration for selection is generally non-deterministic in Go and can be an anti-pattern.
Raw output
Although the impact is negligible here, a more idiomatic and deterministic approach would be to iterate to get the key and then access the value directly. This makes the intent clearer and avoids the non-deterministic pattern. For example: `for k := range a.VersionData.Versions { return a.VersionData.Versions[k], true }`.
}

// Now we check if a default version is defined and will look for it, when NotVersioned is set to false.
// Otherwise, we skip this check.
if !a.VersionData.NotVersioned && a.VersionData.DefaultVersion != "" {
versionInfo, ok = a.VersionData.Versions[a.VersionData.DefaultVersion]
return versionInfo, ok
}

Check notice on line 1726 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: style

style Issue

The comment on line 1723 and the associated condition `if !a.VersionData.NotVersioned` are potentially misleading. The function `GetSingleOrDefaultVersion` is only called from a code path where `NotVersioned` is `true`, making this condition always false in its current usage. This could confuse developers about the function's intended scope and behavior.
Raw output
For better code clarity and maintainability, consider removing the `!a.VersionData.NotVersioned` check and the accompanying comment if this function is exclusively for non-versioned APIs. If it's intended for broader use, the calling logic should be adjusted and the comment clarified.

Check notice on line 1727 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: quality

logic Issue

The condition `!a.VersionData.NotVersioned` within `GetSingleOrDefaultVersion` will always evaluate to false because the function is only called from a context where `NotVersioned` is true. This makes the code block unreachable in its current usage and can be confusing for future maintenance.
Raw output
The comment and the condition should be updated to reflect the actual usage. Since this function is intended for non-versioned APIs, this block appears to be unnecessary. Consider removing it to simplify the function and avoid confusion.
// If no default version is defined, we try to find one named "Default", "default" or ""
if versionInfo, ok = a.VersionData.Versions["Default"]; ok {
return versionInfo, ok
}

if versionInfo, ok = a.VersionData.Versions["default"]; ok {
return versionInfo, ok
}

if versionInfo, ok = a.VersionData.Versions[""]; ok {
return versionInfo, ok
}

// If we reach this point, we tried everything to find a default version and failed
return apidef.VersionInfo{}, false
}

// CheckForAmbiguousDefaultVersions checks if there are multiple ambiguous default versions in the version data.
func (a *APISpec) CheckForAmbiguousDefaultVersions() bool {
foundDefaultVersions := 0
for key := range a.VersionData.Versions {
switch key {
case "Default":
fallthrough
case "default":
fallthrough
case "":
foundDefaultVersions++
}

Check notice on line 1756 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: quality

style Issue

The function uses hardcoded strings "Default", "default", and "" to identify implicit default versions. This pattern is repeated in `CheckForAmbiguousDefaultVersions`. Using magic strings can lead to typos and makes the code harder to maintain if these conventional names need to be changed.
Raw output
Define these values as a slice of constants or variables at the package level (e.g., `var defaultVersionNames = []string{"Default", "default", ""}`) and iterate over that slice in both `GetSingleOrDefaultVersion` and `CheckForAmbiguousDefaultVersions`. This centralizes the logic, improves readability, and follows the DRY (Don't Repeat Yourself) principle.
}

return foundDefaultVersions > 1
}

Check warning on line 1760 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: security

security Issue

The function `CheckForAmbiguousDefaultVersions` iterates over all API versions on every request to a non-versioned API. This introduces an O(N) operation on a critical request path, where N is the number of versions. In a scenario with an extremely large number of versions, this could be a vector for a Denial of Service (DoS) attack.
Raw output
Optimize the check for ambiguity by replacing the inefficient loop with direct map lookups for the specific 'magic' default version names ('Default', 'default', ''). This will make the operation constant time (O(1)) and avoid performance degradation on the hot path, mitigating the theoretical DoS risk.

Check warning on line 1760 in gateway/api_definition.go

View check run for this annotation

probelabs / Visor: performance

performance Issue

The `CheckForAmbiguousDefaultVersions` function iterates over the entire `Versions` map on every request to a non-versioned API. This is an O(N) operation, where N is the number of versions defined for the API. For an API with a large number of versions, this loop can introduce unnecessary latency on a critical request path.
Raw output
Optimize the check for ambiguity to be a constant time (O(1)) operation. Instead of iterating over all keys, perform direct map lookups for the three specific keys that define a default version ("Default", "default", and ""). This removes the performance dependency on the number of versions.

// StripListenPath will strip the listen path from the URL, keeping version in tact.
func (a *APISpec) StripListenPath(reqPath string) string {
return httputil.StripListenPath(a.Proxy.ListenPath, reqPath)
Expand Down
Loading
Loading