Skip to content

feat(networking): opt-in network retry resilience [IDE-1890] - #686

Open
basti-snyk wants to merge 30 commits into
mainfrom
feat/IDE-1890-network-retry-opt-in
Open

feat(networking): opt-in network retry resilience [IDE-1890]#686
basti-snyk wants to merge 30 commits into
mainfrom
feat/IDE-1890-network-retry-opt-in

Conversation

@basti-snyk

@basti-snyk basti-snyk commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

User description

Description

Everything for IDE-1890, consolidated into one PR. All of it sits behind a single opt-in — configuration.NETWORK_REQUEST_RETRIES_ENABLED. With that unset, behaviour is byte-identical to today, pinned by regression tests at every layer.

Four pieces, in dependency order:

1. The opt-in itself (IDE-2412) — new NETWORK_REQUEST_RETRIES_ENABLED config key. When set, the retry middleware's default attempt count becomes 3 without also requiring PREVIEW_FEATURES_ENABLED. An explicitly configured attempt count still wins over both.

2. Transport-error retry (IDE-2419) — today every transport error is wrapped in backoff.Permanent and never retried, whatever the config says. Now ECONNRESET and network timeouts (including DNS timeouts) are retried; DNS NotFound, context cancellation/deadline, and TLS/auth failures are not.

Two details worth a reviewer's attention: the deny-check runs before the timeout allow-check, because context.DeadlineExceeded also satisfies net.Error.Timeout() and a caller-abandoned request must never be retried. And Windows WSAECONNRESET (10054) is matched explicitly — Go does not map it onto syscall.ECONNRESET, so a Unix-only check would pass CI on Linux while silently never retrying on Windows.

Retry eligibility is method- and configuration-based. Safe methods retry by default; mutating methods retry only when their path matches NETWORK_REQUEST_RETRY_ALLOWED_PATHS. The default includes the monitor endpoint paths needed by current consumers, and applications such as the CLI can override the list through Configuration. Matching uses contiguous full path segments, so similarly named segments are unaffected.

3. Truncated-response recovery — the part that fixes the reported customer. Behind an SSL-inspecting corporate proxy, the upstream HTTP/2 stream is reset mid-body after RoundTrip has already returned 200 OK. The truncated body is then parsed as an empty result and the scan reports zero vulnerabilities on a project that has them, roughly 1 session in 5. Because RoundTrip returns before the body is read, no amount of retry configuration reaches this — which is why pieces 1 and 2 alone did not fix it.

The fix reads retryable 2xx JSON bodies inside the existing retry loop; a truncated read then becomes an ordinary retryable error the existing backoff.Retry already handles. The integrity-check buffer has a dedicated 4 MiB cap. Larger JSON bodies preserve the inspected prefix and continue streaming from the original body, while non-JSON responses remain untouched. Three attempts take session success from ~80% to ~99%.

4. Review fixes — the opt-in predicate is extracted to internal/utils.NetworkRetriesEnabled so the two call sites cannot drift, and a nil-guarded drainAndClose closes a response-body leak on the retried transport-error path.

Design context: IDE-2050. CLI flag wiring is deferred to IDE-2415.

Automated review findings

The accumulated review findings have been addressed or evaluated against the final design:

Finding Disposition
DRY: opt-in predicate duplicated across two files Fixed — extracted to internal/utils, not exported, to avoid growing GAF's public API
Response body leak on retried transport error Fixed — note the suggested patch used a bare drainAndClose(response.Body), which panics on the common nil-response transport-error path; the guard checks the response, not just the body
Retry safety for mutating HTTP methods Fixed — safe methods retry by default; mutating methods require a configured allowed path, which preserves the customer POST use case without replaying arbitrary writes
Test constant duplicates the retry-after key Fixed — the middleware owns an exported key alongside its existing exported attempt-count key; tests now use that single definition

Known limits, stated deliberately

  • With the opt-in unset, a truncated response still silently reports zero vulnerabilities. Required, and accepted.
  • PREVIEW_FEATURES_ENABLED also activates all of this, matching the pre-existing attempt-count default. Intentional and tested.
  • Opted-in consumers inspect up to 4 MiB of retryable 2xx JSON bodies inside the retry loop. Larger bodies continue streaming after the buffered prefix, so truncation beyond the cap cannot be retried.
  • Mutating requests are retried only when their paths match the configured allowlist; consumers can override the defaults through Configuration. Blank allowlist entries are ignored so persisted JSON values cannot match every absolute path.
  • getErrorList swallows the identical read error on non-2xx responses. A real adjacent gap, left alone because changing it would alter flag-off behaviour.

Size

~1380 added lines across 9 files, well over the 700-line guideline. Deliberate, and consolidated on request: this is one coherent opt-in feature whose pieces do not stand alone — 2 and 3 are inert without 1, and 3 is the only one that fixes the customer. The large majority is a three-layer test pyramid for correctness-sensitive retry-safety; production code is a small fraction of the diff.

Test plan

  • Acceptance: real engine + real TCP server forcing RST — opted-in caller recovers, non-opted-in caller sees today's behaviour exactly
  • Acceptance: truncated-body recovery, with the flag-off guard confirmed GREEN on baseline before any production code was written
  • Integration: real wiring, including a test that fails if the opt-in OR-condition regresses; monitor path never buffered or retried; non-JSON streams unbuffered; exhausted attempts yield a non-nil error and a non-nil response
  • Unit: error-axis matrix, deny-before-allow ordering, Windows errno, content-type helper, opt-in truth table
  • make format && make lint && make test && make generate; snyk code test — no new findings

Checklist

  • Full make test currently reports two unrelated repository/environment failures (Test_UfmPresenter_HumanReadable fixture drift and TestDetectProxyConfig due the sandbox CA environment); the changed acceptance, integration, and unit paths pass
  • Regenerated mocks, etc. (make generate)
  • Linted (make lint)
  • Test your changes work for the CLI

Note

Medium Risk
Changes shared HTTP client retry semantics and can re-send POSTs on allowlisted paths; incorrect allowlist or buffering could affect scan results or duplicate side effects, though monitor paths are excluded and the feature is opt-in.

Overview
Adds an opt-in resilient network retry policy behind NETWORK_REQUEST_RETRIES_ENABLED (or existing preview features), with flag-off behavior unchanged from today.

Configuration: New keys for the opt-in flag and a path allowlist for retries on unsafe HTTP methods (defaults like test-dep-graph, verify/token, feature_flags/evaluation). Default max attempts (3 vs 1) now follows shared NetworkRetriesEnabled; env CSV and persisted JSON arrays are normalized for the allowlist. GetStringSlice now handles JSON []interface{} from config files.

Retry middleware (when opted in): Retries transient transport errors (e.g. connection reset, network timeouts) instead of always treating RoundTripper errors as permanent. Status-code retries on POST/etc. are limited to allowlisted path segments; safe methods still retry broadly. Successful JSON responses up to ~4MB are buffered in the retry loop so truncated bodies after 200 OK can trigger another attempt; monitor-style paths stay excluded.

Tests: Large integration/unit coverage for opt-in gates, path rules, transport vs truncation recovery, and config wiring through the real app engine.

Reviewed by Cursor Bugbot for commit cbf8bd1. Bugbot is set up for automated code reviews on this repo. Configure here.


PR Type

Enhancement


Description

  • Introduce opt-in for network retry resilience.

  • Enable retries for transient transport errors.

  • Configure retry eligibility by method and path.

  • Maintain byte-identical behavior when disabled.


Diagram Walkthrough

flowchart LR
  A[Client Request] --> B{Retry Middleware};
  B -- Transient Transport Error --> C{Retryable Transport Error?};
  C -- Yes --> D{Request Allowed to Retry?};
  D -- Yes --> B;
  D -- No --> E[Return Error];
  C -- No --> E;
  B -- HTTP Response --> F{Retryable Status Code?};
  F -- Yes --> G{Request Allowed to Retry?};
  G -- Yes --> B;
  G -- No --> H[Return Response];
  F -- No --> H;
Loading

File Walkthrough

Relevant files
Enhancement
6 files
network-retry.go
Add helper function for network retry enablement check.   
+9/-0     
app.go
Integrate retry enablement and define default retry paths.
+22/-1   
configuration.go
Handle []interface{} for GetStringSlice configuration.     
+6/-0     
constants.go
Define new configuration constants for network retries.   
+12/-10 
retry_middleware.go
Implement opt-in network retry logic for transport errors and status
codes.
+73/-4   
retry_transport_error.go
Define retryable transport errors and request criteria.   
+77/-0   
Tests
6 files
network-retry_test.go
Test network retry enablement helper function.                     
+33/-0   
app_test.go
Add comprehensive tests for network retry logic.                 
+740/-0 
configuration_test.go
Test GetStringSlice handling of []interface{}.                     
+33/-0   
netstack_error_handler_test.go
Test Windows WSAECONNRESET error handling.                             
+5/-0     
retry_middleware_test.go
Add extensive tests for retry middleware functionality.   
+849/-19
retry_transport_error_test.go
Test retryable transport error and request logic.               
+179/-0 
Bug fix
1 files
netstack_error_handler.go
Include Windows WSAECONNRESET in connection reset errors.
+7/-3     

…-1890]

Application authors can now enable the framework's multi-attempt network
retry default via NETWORK_REQUEST_RETRIES_ENABLED, independently of
PREVIEW_FEATURES_ENABLED. Behaviour is unchanged for consumers that do
not set it, and an explicitly configured attempt count still wins.

Additive only; no exported symbol or default behaviour is changed.
@basti-snyk
basti-snyk requested review from a team as code owners August 4, 2026 09:44
@snyk-io

snyk-io Bot commented Aug 4, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

basti-snyk commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@snyk-io

snyk-io Bot commented Aug 4, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues
Secrets 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@snyk-pr-review-bot

This comment has been minimized.

…890]

Delete the local boolPtr test helper in app_test.go and use the existing
generic pkg/utils.Ptr helper instead, avoiding duplication. Pure dedup,
no behavior change.
@snyk-pr-review-bot

This comment has been minimized.

Comment thread pkg/configuration/constants.go Outdated
Comment thread pkg/app/app_test.go
Comment thread pkg/app/app_test.go Outdated
@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (d117fa3)

…-2419]

Extend the network-retry opt-in (NETWORK_REQUEST_RETRIES_ENABLED, or
PREVIEW_FEATURES_ENABLED) to also cover transient transport-level
failures - connection resets and network timeouts - which were
previously wrapped in backoff.Permanent and never retried regardless
of retry configuration.

Adds a two-axis allow-list mirroring net/http.Transport's own
isReplayable:
- error axis: connection reset (POSIX ECONNRESET and Windows
  WSAECONNRESET, matched by numeric errno since Go does not map the
  latter onto the former) or a network timeout (net.Error.Timeout());
  DNS NotFound, TLS failures, and caller cancellation/deadline are
  denied. context.DeadlineExceeded also satisfies net.Error.Timeout(),
  so the deny-check runs before the timeout allow-check.
- request axis: safe methods (GET/HEAD/OPTIONS/TRACE, or no method
  set) are always replayable; other methods are replayable only when
  they carry an Idempotency-Key or X-Idempotency-Key header.

New file pkg/networking/middleware/retry_transport_error.go holds the
two unexported predicates. retry_middleware.go's RoundTrip now checks
them (gated behind the opt-in and the remaining attempt budget) before
falling back to the existing unconditional backoff.Permanent behavior.

Outside-in TDD: acceptance tests exercise the real composition root
(CreateAppEngineWithOptions -> GetUnauthorizedHttpClient) against a
real TCP server that simulates a connection reset; integration tests
wire the real RetryMiddleware; unit tests cover the two predicates in
isolation.
…IDE-2419]

Log the transport-error retry at Warn instead of Debug so operators see
it without raising verbosity, per explicit requirement. Driven by TDD:
extended Test_RetryMiddleware_TransportError_ConnResetRetriedWhenOptedIn
to capture zerolog output via a buffer and assert on
`"level":"warn"`, confirmed RED against the prior Debug() call, then
changed Debug() to Warn() in retry_middleware.go and confirmed GREEN.

Remove WHAT-only comments (restating what the following code obviously
does, or just repeating a test's name plus a plan-ID) across
retry_middleware.go, retry_middleware_test.go, retry_transport_error.go,
retry_transport_error_test.go, app_test.go, and constants.go. Genuine
WHY comments (non-obvious design reasons, ordering constraints, numeric
literals, external references) are kept.

Remove duplicate test coverage now that unit tests fully exercise the
classification functions:
- Test_RetryMiddleware_TransportError_ErrorAxis and
  Test_RetryMiddleware_TransportError_RequestAxis trimmed to two rows
  each, enough to prove the middleware is wired to
  isRetryableTransportError/isReplayableRequest without re-testing
  their full branch coverage (already covered by
  Test_isRetryableTransportError and Test_isReplayableRequest).
- Test_RetryMiddleware_TransportError_BodyReplayedOnEveryAttempt
  deleted: Test_TransportRetry_OptIn_RetriedRequestSentInFull now
  asserts every recorded body (not just the last) against
  expectedBody, subsuming it at the acceptance level.
Correction to the prior cleanup commit against this repo's stricter
Rule 8 (comments only for non-obvious WHY, no reasoning/alternatives,
no ticket IDs, no big blocks):

- Cut the connResetErr and fakeTimeoutError rationale comments in
  retry_middleware_test.go: both fixtures are self-explanatory from
  their names and bodies.
- Cut the WHAT-only resettingServerLog doc comment and the
  precedence-matrix table label in app_test.go; both restated what
  is already evident from the field names.
- Condensed the networkRequestRetryAfterSecondsKey and
  newResettingServer comments in app_test.go to their essential WHY
  clause, dropping restated context.
- Shrunk the NETWORK_REQUEST_RETRIES_ENABLED godoc in constants.go
  from a four-line block to a single trailing comment matching its
  neighbors' style; the precedence nuance with an explicit attempts
  count belongs in the ticket, not source.

No ticket-ID references (IDE-1890-*/IDE-2419-*) remain in any of the
six touched files. The errWSAEConnReset rationale, the
context.DeadlineExceeded-before-timeout ordering note, and the
SetLinger(0) RST-vs-FIN note are retained as genuine load-bearing WHY.
@snyk-pr-review-bot

This comment has been minimized.

Comment thread pkg/networking/middleware/retry_middleware.go
Comment thread pkg/networking/middleware/retry_middleware.go Outdated
Comment thread pkg/networking/middleware/retry_middleware.go
Comment thread pkg/app/app_test.go Outdated
…t [IDE-2419]

Replace the method/idempotency-key request axis with an endpoint
deny-list: retry every request when transport-error retry is opted in,
except calls to the monitor endpoint, which creates a snapshot resource
and must not be silently duplicated. The method-based guard refused to
retry POST /v1/test-dep-graph, the customer failure this retry work
exists for; that request creates nothing and is safe to retry.

isReplayableRequest is renamed to isRetryableRequest and now checks the
request path instead of method/headers. The req.Body != nil &&
req.GetBody == nil guard is kept as-is (physical replay constraint, not
a policy choice).

Inverted Test_TransportRetry_OptIn_PostNotRetried into
Test_TransportRetry_OptIn_PostRetried and added
Test_TransportRetry_OptIn_MonitorPathNotRetried. Replaced the two
Test_RetryMiddleware_TransportError_RequestAxis rows and rewrote
Test_isReplayableRequest as Test_isRetryableRequest for the new rule.
@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (d15612f)

@snyk-pr-review-bot

This comment has been minimized.

Comment thread pkg/networking/middleware/retry_transport_error.go Outdated
…eak [IDE-1890]

Two automated PR-reviewer findings on #686:

- The PREVIEW_FEATURES_ENABLED || NETWORK_REQUEST_RETRIES_ENABLED opt-in
  predicate was duplicated between defaultMaxNetworkRequestAttempts (app.go)
  and the transport-error retry check (retry_middleware.go). A future change
  to one site could silently desync the two policies. Extracted to
  internal/utils.NetworkRetriesEnabled, used by both call sites.

- The transport-error retry branch returned a response alongside a
  retryable error without draining/closing its body. backoff.Retry discards
  that response before calling the round tripper again, so any
  http.RoundTripper implementation returning a non-nil response with a
  non-nil error (permitted by the interface contract) would leak the body
  and its connection on every retry. Guarded on response != nil before
  calling drainAndClose, since a transport error commonly carries a nil
  response and drainAndClose only nil-checks the body it's given.
An SSL-inspecting proxy can flush 200 OK headers/status and then have
the upstream connection reset mid-body, leaving RoundTrip returning a
truncated body that gets silently parsed as empty JSON.

When the transport-retry opt-in is active, the request is retryable,
and the response is JSON, RetryMiddleware now reads the body fully and
swaps in a buffered io.ReadCloser before returning. If the read fails,
it returns a *RetryAttemptError so the existing backoff.Retry loop
retries (or fails loudly via backoff.Permanent on the last attempt)
instead of returning a truncated body. Response is always non-nil
alongside the error.

With the opt-in unset, this branch is never entered: no read, no
buffer, no added latency, no changed error surface, pinned by a
regression test.
basti-snyk pushed a commit that referenced this pull request Aug 5, 2026
…eak [IDE-1890]

Two automated PR-reviewer findings on #686:

- The PREVIEW_FEATURES_ENABLED || NETWORK_REQUEST_RETRIES_ENABLED opt-in
  predicate was duplicated between defaultMaxNetworkRequestAttempts (app.go)
  and the transport-error retry check (retry_middleware.go). A future change
  to one site could silently desync the two policies. Extracted to
  internal/utils.NetworkRetriesEnabled, used by both call sites.

- The transport-error retry branch returned a response alongside a
  retryable error without draining/closing its body. backoff.Retry discards
  that response before calling the round tripper again, so any
  http.RoundTripper implementation returning a non-nil response with a
  non-nil error (permitted by the interface contract) would leak the body
  and its connection on every retry. Guarded on response != nil before
  calling drainAndClose, since a transport error commonly carries a nil
  response and drainAndClose only nil-checks the body it's given.
@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@basti-snyk basti-snyk changed the title feat(networking): add opt-in config for resilient retry defaults [IDE-1890] feat(networking): opt-in network retry resilience [IDE-1890] Aug 5, 2026
@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (59b3025)

1 similar comment
@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (59b3025)

Resolve dependency and connection-reset detector conflicts while preserving current main behavior and the shared cross-platform retry classifier.
@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Behavioral Shift in GetStringSlice 🟡 [minor]

The update to GetStringSlice now stringifies non-string elements in a []interface{} using fmt.Sprintf. While this enables reading JSON arrays from config files, it also means that unexpected types (booleans, numbers) will be silently converted to strings instead of being ignored or causing an error. Other callers like IsValidSnykHost in pkg/auth/authHost.go might now receive unexpected string values if the configuration is malformed.

output = append(output, fmt.Sprintf("%v", item))
Potential for Double Close on Truncated Bodies 🟡 [minor]

In RoundTrip, when a JSON response exceeds maxBufferedJSONResponseBytes, the code assigns a multiReadCloser to response.Body. The multiReadCloser.Close() implementation calls m.closer.Close(). However, the original response.Body is already passed as the closer during initialization. If a downstream consumer or another middleware also attempts to close the body, the interaction between the multi-reader's closure and the original closer should be verified to ensure it doesn't cause issues with connection pooling/reuse in http.Transport.

response.Body = &multiReadCloser{Reader: io.MultiReader(bytes.NewReader(bodyBytes), response.Body), closer: response.Body}
📚 Repository Context Analyzed

This review considered 56 relevant code sections from 13 files (average relevance: 0.95)

🤖 Repository instructions applied (from AGENTS.md)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

@basti-snyk

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (102bfe6)

Comment thread pkg/app/app_test.go

assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
assert.Equal(t, int32(3), atomic.LoadInt32(requestCount))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: the 4 tests above are quite similar, consider converting into a table test like this:

func Test_NetworkRetryOptIn(t *testing.T) {
	tests := []struct {
		name            string
		statusSequence  []int
		retriesEnabled  bool
		requestAttempts int // 0 = leave unset
		wantStatus      int
		wantCount       int32
	}{
		{
			name:           "not opted in, single attempt",
			statusSequence: []int{http.StatusServiceUnavailable, http.StatusOK},
			wantStatus:     http.StatusServiceUnavailable,
			wantCount:      1,
		},
		{
			name:            "explicit attempt count wins over opt-in",
			statusSequence:  []int{http.StatusServiceUnavailable, http.StatusOK},
			retriesEnabled:  true,
			requestAttempts: 1,
			wantStatus:      http.StatusServiceUnavailable,
			wantCount:       1,
		},
		{
			name:           "non-retryable response not retried",
			statusSequence: []int{http.StatusNotFound},
			retriesEnabled: true,
			wantStatus:     http.StatusNotFound,
			wantCount:      1,
		},
		{
			name:           "gives up after policy limit",
			statusSequence: []int{http.StatusServiceUnavailable},
			retriesEnabled: true,
			wantStatus:     http.StatusServiceUnavailable,
			wantCount:      3,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server, requestCount := newSequencedStatusServer(t, tt.statusSequence)

			config := configuration.NewWithOpts()
			if tt.retriesEnabled {
				config.Set(configuration.NETWORK_REQUEST_RETRIES_ENABLED, true)
			}
			if tt.requestAttempts != 0 {
				config.Set(middleware.ConfigurationKeyRequestAttempts, tt.requestAttempts)
			}
			config.Set(middleware.ConfigurationKeyRetryAfter, 1)

			engine := CreateAppEngineWithOptions(WithConfiguration(config))
			client := engine.GetNetworkAccess().GetUnauthorizedHttpClient()

			resp, err := client.Get(server.URL)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, tt.wantStatus, resp.StatusCode)
			assert.Equal(t, tt.wantCount, atomic.LoadInt32(requestCount))
		})
	}
}

Comment thread pkg/app/app_test.go

configFile, err := configuration.CreateConfigurationFile("gaf-retry-test.json")
require.NoError(t, err)
require.NoError(t, os.WriteFile(configFile, []byte(`{"internal_network_request_retry_allowed_paths": ["custom-retryable"]}`), 0600))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicks: Remove the hardcodings of the internal key:

require.NoError(t, os.WriteFile(configFile, fmt.Appendf(nil,
    `{%q: ["custom-retryable"]}`, configuration.NETWORK_REQUEST_RETRY_ALLOWED_PATHS), 0600))

Comment thread pkg/app/app_test.go

assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
assert.Equal(t, int32(1), atomic.LoadInt32(requestCount))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: can table test with the test above:

func Test_NetworkRetryOptIn_AllowedPathsFromPersistedJSONConfig(t *testing.T) {
	tests := []struct {
		name        string
		allowlist   []string
		path        string
		wantStatus  int
		wantRetried bool
	}{
		{"declared path is retried", []string{"custom-retryable"}, "/v1/custom-retryable", http.StatusOK, true},
		{"blank entry allows nothing", []string{""}, "/v1/monitor/npm", http.StatusServiceUnavailable, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakehome := t.TempDir()
			t.Setenv("HOME", fakehome)
			t.Setenv("USERPROFILE", fakehome)

			configFile, err := configuration.CreateConfigurationFile("gaf-retry-test.json")
			require.NoError(t, err)
			contents, err := json.Marshal(map[string][]string{
				configuration.NETWORK_REQUEST_RETRY_ALLOWED_PATHS: tt.allowlist,
			})
			require.NoError(t, err)
			require.NoError(t, os.WriteFile(configFile, contents, 0600))

			server, requestCount := newSequencedStatusServer(t, []int{http.StatusServiceUnavailable, http.StatusOK})

			config := configuration.NewWithOpts(
				configuration.WithFiles("gaf-retry-test"),
				configuration.WithSupportedEnvVarPrefixes("snyk_", "internal_"),
				configuration.WithCachingEnabled(configuration.NoCacheExpiration),
			)
			config.Set(configuration.NETWORK_REQUEST_RETRIES_ENABLED, true)
			config.Set(middleware.ConfigurationKeyRetryAfter, 1)

			engine := CreateAppEngineWithOptions(WithConfiguration(config))
			client := engine.GetNetworkAccess().GetUnauthorizedHttpClient()

			req, err := http.NewRequest(http.MethodPost, server.URL+tt.path, bytes.NewReader([]byte(`{}`)))
			require.NoError(t, err)

			resp, err := client.Do(req)
			require.NoError(t, err)
			defer resp.Body.Close()

			assert.Equal(t, tt.wantStatus, resp.StatusCode)
			if tt.wantRetried {
				assert.GreaterOrEqual(t, atomic.LoadInt32(requestCount), int32(2))
			} else {
				assert.Equal(t, int32(1), atomic.LoadInt32(requestCount))
			}
		})
	}
}

for _, item := range v {
output = append(output, fmt.Sprintf("%v", item))
}
return output

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change will make this comment a lie:

// GetStringSlice does not split a plain string value on commas: an env
// var override of CONFIG_KEY_ALLOWED_HOSTS yields a single-element slice
// containing the whole value. See the CONFIG_KEY_ALLOWED_HOSTS doc
// comment for the single-domain-only env var caveat.
Please remove it (and check for others like it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants