Skip to content

fix(listutil): gate optional-resource swallow behind explicit flag#52

Merged
shreemaan-abhishek merged 4 commits into
masterfrom
listutil-allow-optional
May 27, 2026
Merged

fix(listutil): gate optional-resource swallow behind explicit flag#52
shreemaan-abhishek merged 4 commits into
masterfrom
listutil-allow-optional

Conversation

@shreemaan-abhishek
Copy link
Copy Markdown
Contributor

@shreemaan-abhishek shreemaan-abhishek commented May 26, 2026

Closes #50. Stacked on top of #47; please merge #47 first.

Problem

`FetchPaginated` converted every `IsOptionalResourceError` (400/404) into `(nil, nil)`. The leniency was inherited from `configutil`'s original private helper, where it made sense for `stream_routes` / `protos` / `secret_providers` on gateways that don't expose those subsystems. Promoting the helper into a shared package in #42 generalized it to callers that should fail loudly:

  • `route list` aggregation: a 400/404 on one service's `/routes` (e.g. service deleted between enumeration and the per-service fetch) was silently dropped. The user saw a short table with exit 0.
  • `config dump` / `config sync`: a transient 400 on `/services`, `/consumers`, `/ssls` or `/global_rules` returned `nil`. A subsequent `config sync` against the (now-partial) dump would diff an empty side against a populated remote and propose mass deletions.

Also violates AGENTS.md rule 3 (never suppress errors), and the existing doc comment lied — it told callers to "inspect the returned error" on a path where there was no error to inspect.

Change

  • Add explicit `allowOptional bool` to `FetchPaginated` and `FetchRoutesForServices`. Only swallow when `true`.
  • Rewrite both doc comments to describe the real contract.
  • Update call sites per the table in listutil: FetchPaginated silently swallows 400/404 instead of propagating #50:
    • `configutil`: `true` for `stream_routes`, `protos`, `secret_providers`; `false` for `services`, `consumers`, `ssls`, `global_rules`, and the per-service routes call.
    • `route list`: `false` on both call sites.

`fetchPluginMetadata` doesn't go through `FetchPaginated` (it hand-rolls calls to `/apisix/admin/plugins/list` and `/apisix/admin/plugin_metadata/{name}`), so it already handles its own leniency locally and needs no change.

Tests

New file `pkg/listutil/listutil_test.go` with 7 cases:

  • `FetchPaginated` 404 + `allowOptional=false` → error propagates as `*api.APIError` with status 404.
  • `FetchPaginated` 400 + `allowOptional=false` → error propagates.
  • `FetchPaginated` 404 + `allowOptional=true` → `(nil, nil)`.
  • `FetchPaginated` 400 + `allowOptional=true` → `(nil, nil)`.
  • `FetchPaginated` 500 + `allowOptional=true` → still propagates (allowOptional only relaxes 400/404).
  • `FetchRoutesForServices` per-service 404 + `allowOptional=false` → propagates.
  • `FetchRoutesForServices` per-service 404 + `allowOptional=true` → skips, returns surviving service's routes.

Test plan

  • `go build ./...`
  • `go vet ./...`
  • `make test` — full suite passes locally, including `pkg/cmd/route/list`, `pkg/cmd/config/diff`, `pkg/cmd/config/dump`, `pkg/cmd/config/sync`.
  • `make lint` — fails on 3 pre-existing `yaml.*` typecheck errors in `pkg/cmdutil/{exporter,fileutil}.go` and `pkg/cmd/config/configutil/configutil.go:99`. Verified against the parent branch `route-list-aggregate`: same failures, so not introduced by this PR.

Summary by CodeRabbit

  • Bug Fixes
    • Improved error handling during configuration and route fetching to gracefully handle missing or disabled optional resources.
    • System now distinguishes between required and optional resources, allowing operations to continue when optional components are unavailable while still enforcing failures for critical resources.

Review Change Stack

FetchPaginated previously converted any IsOptionalResourceError (400/404)
into (nil, nil) for every caller. The lenience was inherited from
configutil's private helper, where it was intentional for stream_routes,
protos and secret_providers in non-stream deployments; promoting the
helper to a shared package generalized it to consumers that need strict
behavior:

  * route list aggregation across services silently dropped a deleted
    service's 404 and rendered a short table with no error.
  * config dump / config sync trusted (nil, nil) on services /
    consumers / ssl / global_rules, producing partial output that the
    diff engine would then treat as remote deletions or duplicate
    creates against an out-of-sync gateway.

Add an explicit allowOptional bool to FetchPaginated and thread it
through FetchRoutesForServices. Set true at the three configutil call
sites that genuinely tolerate the endpoint being absent
(stream_routes, protos, secret_providers); set false everywhere else.

Closes #50
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

This PR adds an allowOptional boolean parameter to pkg/listutil.FetchPaginated and FetchRoutesForServices to control whether HTTP 400/404 errors are suppressed or propagated. Call sites specify the flag based on resource type: false for required resources and true for optional ones. Comprehensive tests validate both behaviors.

Changes

Optional resource error handling refactor

Layer / File(s) Summary
FetchPaginated and FetchRoutesForServices signature and behavior updates
pkg/listutil/listutil.go
FetchPaginated gains allowOptional parameter; when true, HTTP 400/404 errors on optional resources return (nil, nil), otherwise errors propagate. FetchRoutesForServices gains the same parameter and threads it to per-service FetchPaginated calls. Error handling path is now conditional on the flag value.
Test coverage for allowOptional behavior
pkg/listutil/listutil_test.go
New test suite covers FetchPaginated with both allowOptional=false (errors propagate) and allowOptional=true (400/404 suppressed, 5xx still propagate). Tests for FetchRoutesForServices verify per-service 404 behavior matches the flag: strict mode propagates, optional mode skips the failing service. Test client helper uses httpmock for response mocking.
Update configutil and route list call sites
pkg/cmd/config/configutil/configutil.go, pkg/cmd/route/list/list.go
configutil passes false for required resources (services, consumers, ssl, global_rules, routes) and true for optional ones (stream_routes, protos, secrets). route list passes false to enforce strict error propagation during route aggregation.

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • api7/a7#47: Both PRs modify the same route-list aggregation implementation by introducing/using pkg/listutil.FetchPaginated and FetchRoutesForServices from pkg/cmd/config/configutil/configutil.go and pkg/cmd/route/list/list.go; the main PR further changes their signatures/behavior via the new allowOptional flag.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning PR has only unit tests; lacks E2E test coverage per check requirement #1. No integration tests for FetchRemoteConfig or route list per-service errors. Review comment unaddressed. Add E2E tests: (1) FetchRemoteConfig with optional/required resources, (2) Route list with per-service 404, (3) Config dump exercising allowOptional flags. End-to-end, not unit-only.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(listutil): gate optional-resource swallow behind explicit flag' directly matches the primary change: adding an explicit allowOptional bool parameter to gate the lenient error-swallowing behavior.
Linked Issues check ✅ Passed All coding objectives from issue #50 are met: allowOptional parameter added to FetchPaginated and FetchRoutesForServices, true/false flags correctly wired at call sites (true for stream_routes/protos/secrets, false for services/consumers/ssl/global_rules/route list), doc comments updated, and comprehensive unit tests added.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #50 objectives: parameter addition, call-site updates, documentation, and tests. No unrelated refactoring, cleanup, or feature additions present.
Security Check ✅ Passed allowOptional flag correctly gates 400/404 suppression only; authorization errors never suppressed. Proper flag mapping for optional vs required resources. Tests verify error handling contract.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch listutil-allow-optional

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the shared pkg/listutil pagination/aggregation helpers so that swallowing “optional resource” API errors (HTTP 400/404) is only possible when explicitly opted into, preventing silent partial results in commands like route list and config dump/sync.

Changes:

  • Added an allowOptional bool parameter to FetchPaginated and threaded it through FetchRoutesForServices.
  • Updated route list and configutil.FetchRemoteConfig call sites to pass allowOptional=false for required resources and per-service route fetches; kept true only for genuinely optional resources (stream_routes, protos, secret_providers).
  • Added unit tests covering strict vs. optional behavior for both helpers.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
pkg/listutil/listutil.go Adds allowOptional gating for optional-resource error swallowing and updates helper contracts.
pkg/listutil/listutil_test.go Introduces unit tests for strict/lenient behavior of pagination and per-service route aggregation.
pkg/cmd/route/list/list.go Updates route listing to use strict behavior (allowOptional=false) for both direct and aggregated list paths.
pkg/cmd/config/configutil/configutil.go Ensures config dump/sync uses strict behavior for required resources and explicit leniency only for optional endpoints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/listutil/listutil.go Outdated
Comment on lines +23 to +24
// in use on the target gateway (stream_routes, protos, secret_providers,
// plugin_metadata). All other callers must pass false so transient failures
Comment on lines +19 to +24
func TestFetchPaginated_StrictPropagates404(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(http.MethodGet, "/apisix/admin/services", httpmock.StringResponse(http.StatusNotFound, `{"error_msg":"not found"}`))

client := newTestClient(reg)
items, err := FetchPaginated[api.Service](client, "/apisix/admin/services", nil, false)
- Drop plugin_metadata from the FetchPaginated doc comment; it never
  goes through FetchPaginated (configutil.fetchPluginMetadata hand-rolls
  /plugins/list + /plugin_metadata/{name}).
- Call reg.Verify(t) in every listutil test so a regression that
  short-circuits before issuing the HTTP request can't pass silently.
@shreemaan-abhishek shreemaan-abhishek changed the base branch from route-list-aggregate to master May 27, 2026 01:57
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/cmd/route/list/list.go (1)

128-136: ⚡ Quick win

Add a command-path test for strict error propagation in fetchRoutes.

allowOptional=false is now part of this command’s contract. Please add a test that verifies /routes or /services 400/404 errors are surfaced (not swallowed) in the route-list path.

As per coding guidelines: **/*.go: Write tests for every code change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/route/list/list.go` around lines 128 - 136, Add a unit test that
ensures fetchRoutes (the code path invoking listutil.FetchPaginated[api.Route]
and listutil.FetchPaginated[api.Service] and then
listutil.FetchRoutesForServices) does not swallow HTTP 400/404 errors when
allowOptional=false; mock the HTTP client to return 400 or 404 from the
/apisix/admin/routes and /apisix/admin/services endpoints and assert that the
command returns an error (propagates the error) instead of succeeding or
returning empty results, using the same routeQuery/baseQuery parameters used in
the function under test so the test exercises the exact command-path behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/cmd/config/configutil/configutil.go`:
- Around line 115-147: FetchRemoteConfig is not covered by tests to ensure the
boolean allowOptional mapping is passed correctly to listutil functions; add a
unit test for FetchRemoteConfig that stubs/mocks listutil.FetchPaginated and
listutil.FetchRoutesForServices and asserts the boolean argument values: ensure
calls for services, consumers, ssl, global_rules pass allowOptional=false while
calls for stream_routes, protos, secret_providers pass allowOptional=true (and
routes are fetched via FetchRoutesForServices with false), and verify the
aggregated return value matches expected output to lock the contract.

---

Nitpick comments:
In `@pkg/cmd/route/list/list.go`:
- Around line 128-136: Add a unit test that ensures fetchRoutes (the code path
invoking listutil.FetchPaginated[api.Route] and
listutil.FetchPaginated[api.Service] and then listutil.FetchRoutesForServices)
does not swallow HTTP 400/404 errors when allowOptional=false; mock the HTTP
client to return 400 or 404 from the /apisix/admin/routes and
/apisix/admin/services endpoints and assert that the command returns an error
(propagates the error) instead of succeeding or returning empty results, using
the same routeQuery/baseQuery parameters used in the function under test so the
test exercises the exact command-path behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1ae4a49b-cca9-4306-b050-13424ebd8cd4

📥 Commits

Reviewing files that changed from the base of the PR and between f75246c and 603125b.

📒 Files selected for processing (4)
  • pkg/cmd/config/configutil/configutil.go
  • pkg/cmd/route/list/list.go
  • pkg/listutil/listutil.go
  • pkg/listutil/listutil_test.go

Comment on lines +115 to 147
services, err := listutil.FetchPaginated[api.Service](client, "/apisix/admin/services", query, false)
if err != nil {
return nil, err
}

// API7 EE requires service_id when listing routes with access tokens.
// Fetch routes per service and aggregate.
routes, err := listutil.FetchRoutesForServices(client, services, query)
routes, err := listutil.FetchRoutesForServices(client, services, query, false)
if err != nil {
return nil, err
}
consumers, err := listutil.FetchPaginated[api.Consumer](client, "/apisix/admin/consumers", query)
consumers, err := listutil.FetchPaginated[api.Consumer](client, "/apisix/admin/consumers", query, false)
if err != nil {
return nil, err
}
ssl, err := listutil.FetchPaginated[api.SSL](client, "/apisix/admin/ssls", query)
ssl, err := listutil.FetchPaginated[api.SSL](client, "/apisix/admin/ssls", query, false)
if err != nil {
return nil, err
}
globalRules, err := listutil.FetchPaginated[api.GlobalRule](client, "/apisix/admin/global_rules", query)
globalRules, err := listutil.FetchPaginated[api.GlobalRule](client, "/apisix/admin/global_rules", query, false)
if err != nil {
return nil, err
}
streamRoutes, err := listutil.FetchPaginated[api.StreamRoute](client, "/apisix/admin/stream_routes", query)
streamRoutes, err := listutil.FetchPaginated[api.StreamRoute](client, "/apisix/admin/stream_routes", query, true)
if err != nil {
return nil, err
}
protos, err := listutil.FetchPaginated[api.Proto](client, "/apisix/admin/protos", query)
protos, err := listutil.FetchPaginated[api.Proto](client, "/apisix/admin/protos", query, true)
if err != nil {
return nil, err
}
secrets, err := listutil.FetchPaginated[api.Secret](client, "/apisix/admin/secret_providers", query)
secrets, err := listutil.FetchPaginated[api.Secret](client, "/apisix/admin/secret_providers", query, true)
if err != nil {
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 27, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add call-site tests for allowOptional mapping in FetchRemoteConfig.

This wiring is correctness-critical, but the provided tests only validate listutil behavior, not this mapping (false for required resources, true for optional ones). Please add a focused FetchRemoteConfig test to lock this contract and prevent silent regressions.

As per coding guidelines: **/*.go: Write tests for every code change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/cmd/config/configutil/configutil.go` around lines 115 - 147,
FetchRemoteConfig is not covered by tests to ensure the boolean allowOptional
mapping is passed correctly to listutil functions; add a unit test for
FetchRemoteConfig that stubs/mocks listutil.FetchPaginated and
listutil.FetchRoutesForServices and asserts the boolean argument values: ensure
calls for services, consumers, ssl, global_rules pass allowOptional=false while
calls for stream_routes, protos, secret_providers pass allowOptional=true (and
routes are fetched via FetchRoutesForServices with false), and verify the
aggregated return value matches expected output to lock the contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Holding off on this one. The mapping is exercised indirectly by the existing pkg/cmd/config/dump and pkg/cmd/config/sync test suites (both call FetchRemoteConfig, both still pass), and the new pkg/listutil tests lock the helper's strict/lenient contract on each side. The flag table is also documented in #50, the PR body, and the commit message.

To assert the literal boolean argument at each call site we'd need to either inject a fetcher interface into configutil or introduce a test seam over the package-level listutil.FetchPaginated. That's a non-trivial refactor for an assertion that catches one failure mode (an accidental boolean flip), so I'd rather leave it out of this PR. Happy to revisit if the mapping becomes more dynamic in the future.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@shreemaan-abhishek, that's a reasonable trade-off — the indirect coverage from the dump/sync suites plus the listutil unit tests gives a solid safety net, and the refactor cost for injecting a fetcher seam isn't justified for a single boolean-flip guard right now. I'll drop this suggestion.

Would you like me to open a follow-up issue to track the potential future refactor (interface injection into configutil) for when the mapping becomes more dynamic or the call-site count grows?


🧠 Learnings used
Learnt from: shreemaan-abhishek
Repo: api7/a7 PR: 43
File: pkg/cmdutil/exporter_test.go:1-1
Timestamp: 2026-05-26T06:17:24.351Z
Learning: In the api7/a7 repository, Apache 2.0 license headers are intentionally not required on Go source files, including *_test.go files. During code reviews, do not flag issues for missing Apache 2.0 license headers in any .go files.

@shreemaan-abhishek shreemaan-abhishek merged commit fd336df into master May 27, 2026
6 checks passed
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.

listutil: FetchPaginated silently swallows 400/404 instead of propagating

2 participants