Skip to content

Conversation

buger
Copy link
Member

@buger buger commented Oct 1, 2025

User description

TT-15863 fix random version picking for not versioned API (#7380)

TT-15863
Summary Random version being picked when not_versioned is set to true
Type Bug Bug
Status In Dev
Points N/A
Labels -

This PR fixes an issue where a random version was picked for a
non-versioned API.

Instead of picking a random version for non-versioned APIs it will now:

  1. Use the only defined version
  2. Use the version called "Default", "default" or "" when multiple
    versions are present
  3. Return error when multiple default versions are found
  4. Return error when no version exists

PR Type

Bug fix, Tests


Description

  • Deterministic version selection for non-versioned APIs

  • New request statuses for default version issues

  • Helper methods to resolve default/ambiguous versions

  • Comprehensive unit tests for new logic


Diagram Walkthrough

flowchart LR
  VersionReq["APISpec.Version(request)"]
  CheckAmb["CheckForAmbiguousDefaultVersions()"]
  GetDef["GetSingleOrDefaultVersion()"]
  Ok["Return StatusOk with version"]
  ErrAmb["Return VersionAmbiguousDefault"]
  ErrDef["Return VersionDefaultForNotVersionedNotFound"]

  VersionReq -- "NotVersioned == true" --> CheckAmb
  CheckAmb -- "ambiguous" --> ErrAmb
  CheckAmb -- "not ambiguous" --> GetDef
  GetDef -- "found" --> Ok
  GetDef -- "not found" --> ErrDef
Loading

File Walkthrough

Relevant files
Bug fix
api_definition.go
Deterministic version resolution and new statuses               

gateway/api_definition.go

  • Add new RequestStatus values for default version errors
  • Implement GetSingleOrDefaultVersion helper
  • Implement CheckForAmbiguousDefaultVersions helper
  • Update Version() to use deterministic selection for non-versioned APIs
+92/-34 
Tests
api_definition_test.go
Unit tests for version resolution logic                                   

gateway/api_definition_test.go

  • Add tests for GetSingleOrDefaultVersion
  • Add tests for CheckForAmbiguousDefaultVersions
  • Add tests for Version() with not_versioned scenarios
  • Validate new error statuses and outcomes
+317/-0 

<details open>
<summary><a href="https://tyktech.atlassian.net/browse/TT-15863"
title="TT-15863" target="_blank">TT-15863</a></summary>
  <br />
  <table>
    <tr>
      <th>Summary</th>
<td>Random version being picked when not_versioned is set to true</td>
    </tr>
    <tr>
      <th>Type</th>
      <td>
<img alt="Bug"
src="https://tyktech.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium"
/>
        Bug
      </td>
    </tr>
    <tr>
      <th>Status</th>
      <td>In Dev</td>
    </tr>
    <tr>
      <th>Points</th>
      <td>N/A</td>
    </tr>
    <tr>
      <th>Labels</th>
      <td>-</td>
    </tr>
  </table>
</details>
<!--
  do not remove this marker as it will break jira-lint's functionality.
  added_by_jira_lint
-->

---

This PR fixes an issue where a random version was picked for a
non-versioned API.

Instead of picking a random version for non-versioned APIs it will now:
 1. Use the only defined version
2. Use the version called "Default", "default" or "" when multiple
versions are present
 3. Return error when multiple default versions are found
 4. Return error when no version exists

(cherry picked from commit 14c6d8f)
Copy link
Contributor

github-actions bot commented Oct 1, 2025

API Changes

--- prev.txt	2025-10-01 13:32:46.832019867 +0000
+++ current.txt	2025-10-01 13:32:37.438030138 +0000
@@ -9140,6 +9140,10 @@
 func (s *APISpec) AddUnloadHook(hook func())
     AddUnloadHook adds a function to be called when the API spec is unloaded
 
+func (a *APISpec) CheckForAmbiguousDefaultVersions() bool
+    CheckForAmbiguousDefaultVersions checks if there are multiple ambiguous
+    default versions in the version data.
+
 func (a *APISpec) CheckSpecMatchesStatus(r *http.Request, rxPaths []URLSpec, mode URLStatus) (bool, interface{})
     CheckSpecMatchesStatus checks if a URL spec has a specific status.
     Deprecated: The function doesn't follow go return conventions (T, ok);
@@ -9159,6 +9163,11 @@
     takes the precedence. If the global one is `true`, value of the one in api
     level doesn't matter.
 
+func (a *APISpec) GetSingleOrDefaultVersion() (versionInfo apidef.VersionInfo, ok bool)
+    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) GetTykExtension() *oas.XTykAPIGateway
 
 func (a *APISpec) Init(authStore, sessionStore, healthStore, orgStore storage.Handler)
@@ -11392,36 +11401,38 @@
     RequestStatus is a custom type to avoid collisions
 
 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"
 )
     Statuses of the request, all are false-y except StatusOk and
     StatusOkAndIgnore

Copy link
Contributor

github-actions bot commented Oct 1, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Fix misleading comment logic

The comment contradicts the code path and can mislead future maintenance. Align the
comment with the actual behavior so readers know the default is only honored when
NotVersioned is false. Incorrect comments around version selection can cause
critical misconfigurations.

gateway/api_definition.go [1721-1726]

-// 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 API is versioned (NotVersioned == false) and a DefaultVersion is set, return that specific version.
 if !a.VersionData.NotVersioned && a.VersionData.DefaultVersion != "" {
 	versionInfo, ok = a.VersionData.Versions[a.VersionData.DefaultVersion]
 	return versionInfo, ok
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly aligns the comment with the implemented condition (!NotVersioned with non-empty DefaultVersion), improving clarity without changing behavior. It's a minor but valid documentation fix.

Low
Canonicalize default key detection

Treat Default, default, and "" as a single canonical default to avoid ambiguity due
to case or empty key duplicates. Normalize keys while counting so a single logical
default doesn't spuriously trigger ambiguity when both Default and default exist but
map to the same intent.

gateway/api_definition.go [1745-1760]

 func (a *APISpec) CheckForAmbiguousDefaultVersions() bool {
-	foundDefaultVersions := 0
+	seen := map[string]bool{}
 	for key := range a.VersionData.Versions {
 		switch key {
-		case "Default":
-			fallthrough
-		case "default":
-			fallthrough
+		case "Default", "default":
+			seen["default"] = true
 		case "":
-			foundDefaultVersions++
+			seen["empty"] = true
 		}
 	}
-
-	return foundDefaultVersions > 1
+	// Ambiguous only if more than one logical default variant is present
+	return len(seen) > 1
 }
Suggestion importance[1-10]: 4

__

Why: The proposal changes ambiguity detection semantics by considering case-variants as one logical default, conflicting with the new tests that expect ambiguity when both exist. It could reduce false positives in some setups but contradicts the PR's chosen behavior.

Low
Possible issue
Honor explicit default for non-versioned

For not versioned APIs, GetSingleOrDefaultVersion ignores DefaultVersion even if
set, which may surprise operators and differ from prior behavior. Consider
preferring an explicitly configured DefaultVersion when present, falling back to the
name heuristics only if not set.

gateway/api_definition.go [1668-1678]

 if a.VersionData.NotVersioned {
 	ambiguous := a.CheckForAmbiguousDefaultVersions()
 	if ambiguous {
 		return nil, VersionAmbiguousDefault
+	}
+
+	// Prefer explicitly configured DefaultVersion if provided
+	if a.VersionData.DefaultVersion != "" {
+		if v, ok := a.VersionData.Versions[a.VersionData.DefaultVersion]; ok {
+			version = v
+			ctxSetVersionInfo(r, &version)
+			return &version, StatusOk
+		}
 	}
 
 	ok := false
 	version, ok = a.GetSingleOrDefaultVersion()
 	if !ok {
 		return nil, VersionDefaultForNotVersionedNotFound
 	}
 } else {
-  // ...
+	// ...
 }
Suggestion importance[1-10]: 5

__

Why: This proposes a behavior change to prefer DefaultVersion even when NotVersioned is true; while potentially useful, it diverges from the newly added logic and tests that intentionally ignore DefaultVersion in this case. It's a plausible enhancement but not aligned with the PR's intent.

Low

Copy link

probelabs bot commented Oct 1, 2025

🔍 Code Analysis Results

🚀 Pull Request Analysis: Fix for Random Version Picking in Non-Versioned APIs

This pull request addresses a bug where the Tyk Gateway would select a random version for an API marked as not_versioned. The new implementation introduces a deterministic and predictable logic for version selection in such cases, improving consistency and reliability.

1. Change Impact Analysis

What this PR Accomplishes

The primary goal of this PR is to fix incorrect behavior for non-versioned APIs that have multiple versions defined. Previously, the gateway would arbitrarily pick one of the available versions. This change ensures that a specific, default version is chosen based on a clear set of rules, or an error is returned if a default version cannot be determined.

Key Technical Changes

The core changes are located in gateway/api_definition.go, where the logic for version selection has been significantly improved.

  1. Modified APISpec.Version() Method:

    • For APIs with NotVersioned: true, the logic no longer picks the first available version.
    • It now first checks for ambiguity (i.e., multiple "default" versions) using the new CheckForAmbiguousDefaultVersions() method.
    • If no ambiguity is found, it attempts to find a single or default version using the new GetSingleOrDefaultVersion() method.
  2. New Function: GetSingleOrDefaultVersion(): This function encapsulates the new logic for finding the correct version for a non-versioned API:

    • If only one version is defined, it is returned.
    • If multiple versions exist, it searches for a version named "Default", "default", or "" (empty string).
    • If a specific DefaultVersion is set in the API definition (and NotVersioned is false), that version is used.
    • If no suitable version is found, it returns ok = false.
  3. New Function: CheckForAmbiguousDefaultVersions(): This helper function prevents ambiguity by checking if more than one potential default version (e.g., both "Default" and "default") exists, which would make the selection process non-deterministic.

  4. New Error Statuses: Two new RequestStatus constants have been added to provide clear feedback when version selection fails:

    • VersionDefaultForNotVersionedNotFound: Returned when no default version can be found for a non-versioned API.
    • VersionAmbiguousDefault: Returned when multiple conflicting default versions are found.
  5. Comprehensive Testing: The file gateway/api_definition_test.go has been updated with an extensive set of new unit tests to validate the new logic, covering various scenarios including single versions, multiple versions, ambiguous defaults, and no defaults.

Affected System Components

The change directly impacts the Tyk Gateway's request processing pipeline. Specifically, it modifies the initial step of resolving which API version should handle an incoming request. This is a critical component for routing and applying version-specific policies.

2. Architecture Visualization

The following flowchart illustrates the updated decision-making process within the APISpec.Version() method when handling a request for a non-versioned API (NotVersioned: true).

graph TD
    A[Start: Request for Non-Versioned API] --> B{"Is &#39;NotVersioned&#39; true?"};
    B -->|Yes| C{Check for Ambiguous Defaults};
    B -->|No| H[Proceed with Standard Version Extraction];
    C -->|Ambiguous| D["Return &#39;VersionAmbiguousDefault&#39; Error"];
    C -->|Not Ambiguous| E[Get Single or Default Version];
    E -->|Found| F["Return Version & &#39;StatusOk&#39;"];
    E -->|Not Found| G["Return &#39;VersionDefaultForNotVersionedNotFound&#39; Error"];

    subgraph "New Logic for Non-Versioned APIs"
        C
        D
        E
        F
        G
    end

    style D fill:#f9f,stroke:#333,stroke-width:2px
    style G fill:#f9f,stroke:#333,stroke-width:2px
    style F fill:#9f9,stroke:#333,stroke-width:2px
Loading

Powered by Visor from Probelabs

Last updated: 2025-10-01T13:38:58.416Z | Triggered by: opened | Commit: 3381147

Copy link

probelabs bot commented Oct 1, 2025

🔍 Code Analysis Results

✅ Security Check Passed

No security issues found – changes LGTM.

Performance Issues (1)

Severity Location Issue
🟠 Error gateway/api_definition.go:1754-1768
The `CheckForAmbiguousDefaultVersions` function iterates through all API versions on every request to a non-versioned API. This introduces an O(N) complexity into the critical request path, where N is the number of versions. This check should be performed only once when the API definition is loaded, not on every request.
💡 SuggestionPre-calculate whether there are ambiguous default versions when the API definition is loaded and store the result in a new field within the `APISpec` or `apidef.VersionData` struct. The `Version` method can then check this boolean field, avoiding the expensive iteration on each call.

Similarly, the logic in GetSingleOrDefaultVersion to find the default version for a non-versioned API can also be executed once at load time, and the result cached. This would make version resolution for non-versioned APIs an O(1) operation.

Quality Issues (2)

Severity Location Issue
🟠 Error gateway/api_definition.go:1670-1673
API definition validation logic is being executed on the critical request path. The `CheckForAmbiguousDefaultVersions` function, which validates the API configuration for multiple default versions, is called for every request to a non-versioned API. This mixes request-time logic with load-time validation, impacting both performance and maintainability.
💡 SuggestionMove the validation logic to the API loading and processing phase. When an `APISpec` is created or updated, pre-calculate the default version for non-versioned APIs and check for ambiguity. Store the result (either the resolved default version or an error/flag indicating ambiguity) in the `APISpec` struct. The `Version` method should then use this pre-calculated value, making the request-time operation O(1) and ensuring that configuration errors are caught early.
🟡 Warning gateway/api_definition.go:1726-1734
The logic to find an implicit default version by checking for keys 'Default', 'default', and '' involves repetitive code blocks.
💡 SuggestionRefactor the repeated checks into a loop to improve readability and maintainability. This makes the code more concise and easier to modify if more default version names are supported in the future.
🔧 Suggested Fix
	// If no default version is defined, we try to find one by looping through a list of possible names
	for _, defaultName := range []string{"Default", "default", ""} {
		if versionInfo, ok = a.VersionData.Versions[defaultName]; ok {
			return versionInfo, ok
		}
	}

Style Issues (1)

Severity Location Issue
🟢 Info gateway/api_definition.go:1720-1730
The series of `if` statements to check for default version names (`"Default"`, `"default"`, `""`) is clear and explicit, but could be made more concise.
💡 SuggestionFor a slightly more concise implementation, you could consider iterating over a slice of these default names. This is a minor stylistic suggestion, and the current implementation is perfectly readable.
🔧 Suggested Fix
// If no default version is defined, we try to find one by common names
defaultNames := []string{"Default", "default", ""}
for _, name := range defaultNames {
    if v, ok := a.VersionData.Versions[name]; ok {
        return v, true
    }
}

Dependency Issues (2)

Severity Location Issue
🔴 Critical gateway/api_definition.go:1669
The change in version selection for non-versioned APIs is a behavioral breaking change that will affect how API definitions are validated and managed in `tyk-operator`. An API definition that was previously valid may now be rejected by the gateway. The `tyk-operator`'s validation logic for the `ApiDefinition` CRD must be updated to align with this new behavior.
💡 SuggestionCreate a corresponding pull request in the `tyk-operator` repository to update its validation logic and link it to this PR. The operator should enforce that a non-versioned API with multiple versions has exactly one default version (named 'Default', 'default', or '').
🔴 Critical gateway/api_definition.go:1669
The new logic for selecting a version for non-versioned APIs will impact the Tyk Developer Portal. Users can currently create API definitions in the portal that will be rejected by the gateway with this change. This will lead to a poor user experience.
💡 SuggestionCreate a corresponding pull request in the `portal` repository to update the API Designer's UI and validation logic. The portal should guide users to set a default version when creating a non-versioned API with multiple versions. Link that PR to this one.

Connectivity Issues (2)

Severity Location Issue
🔴 Critical gateway/api_definition.go:1671
The change in version selection for non-versioned APIs introduces a behavioral breaking change that requires updates in `tyk-operator`. Previously, any non-versioned API with multiple versions would load one of them non-deterministically. Now, it will fail to load unless a specific default version ('Default', 'default', or '') exists. `tyk-operator`'s validation for the `ApiDefinition` CRD must be updated to enforce this new requirement, otherwise users may apply configurations that the Gateway will reject at runtime, leading to service disruption.
💡 SuggestionA corresponding pull request for the `tyk-operator` repository should be created and linked. This PR should update the validation logic for the `ApiDefinition` custom resource to align with the new gateway behavior for non-versioned APIs.
🔴 Critical gateway/api_definition.go:1671
The new, stricter validation for non-versioned APIs requires corresponding changes in the Tyk Portal. The Portal UI and backend validation logic must be updated to guide users in creating valid API definitions. Without this, users could configure an API in the Portal that appears valid but is rejected by the Gateway, causing confusion and preventing the API from loading.
💡 SuggestionA corresponding pull request for the `portal` repository should be created and linked. This PR should update the API designer's validation to enforce that a non-versioned API with multiple versions must have one named 'Default', 'default', or ''.

Powered by Visor from Probelabs

Last updated: 2025-10-01T13:38:59.401Z | Triggered by: opened | Commit: 3381147

Copy link

sonarqubecloud bot commented Oct 1, 2025

@pvormste pvormste merged commit dd25fbe into release-5.10 Oct 2, 2025
71 of 76 checks passed
@pvormste pvormste deleted the merge/release-5.10/14c6d8ff6dddf6bf026969ad824f1daa8a3f6d1b branch October 2, 2025 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants