Restore eager NuGet service index discovery - #240
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Eager discovery can currently panic on empty/short service-index bodies and also attempts to parse some non-2xx responses as service indexes, which risks startup failures and incorrect discovery.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (3)
| Severity | Finding |
|---|---|
internal/handlers/nuget_feed.go — extraUrlsFromSourceResponse can panic on empty or short bodies: the empty-body guard was removed… |
|
internal/handlers/nuget_feed.go — discoverNugetFeedURLsForJob currently treats non-error non-2xx responses (e.g., 3xx without… |
|
internal/handlers/nuget_feed_test.go — The updated NuGet discovery parsing no longer has regression coverage for empty/short bodies;… |
What changed in this PR
Restores eager NuGet service-index discovery during proxy startup (undoing the deferred-on-response approach from #203) so NuGet resource URLs are registered before the proxy begins serving traffic, while using bounded concurrency and preserving configured credential ordering.
Changes:
- Move NuGet service-index/resource discovery back into
NewNugetFeedHandler, using a bounded worker pool and merging results deterministically. - Remove the deferred discovery request/response hooks from the proxy pipeline and NuGet handler.
- Update NuGet and OIDC handler tests to mock constructor-time discovery and add a concurrency regression test.
| File | Description |
|---|---|
| proxy.go | Removes deferred NuGet discovery hook wiring and registers NuGet handler in the normal request chain. |
| internal/handlers/oidc_handling_test.go | Adds URL-level HTTP mocks to support NuGet constructor-time discovery for OIDC cases. |
| internal/handlers/nuget_feed.go | Reworks NuGet handler to eagerly discover service index resources concurrently during construction. |
| internal/handlers/nuget_feed_test.go | Updates NuGet handler tests for eager discovery and adds a concurrency/ordering regression test. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Eager discovery currently fails for scheme-less configured NuGet service-index URLs (missing protocol scheme), which can prevent required authenticated resource URL discovery at startup.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
internal/handlers/nuget_feed.go — Scheme-less NuGet service-index URLs (e.g. "nuget.example.com/v3/index.json") are accepted… |
Issues resolved since last review (3)
| Severity | Finding |
|---|---|
internal/handlers/nuget_feed_test.go — The updated NuGet discovery parsing no longer has regression coverage for empty/short bodies;… View resolved comment |
|
internal/handlers/nuget_feed.go — discoverNugetFeedURLsForJob currently treats non-error non-2xx responses (e.g., 3xx without… View resolved comment |
|
internal/handlers/nuget_feed.go — extraUrlsFromSourceResponse can panic on empty or short bodies: the empty-body guard was removed… View resolved comment |
Suppressed comments (1)
internal/handlers/nuget_feed.go:130
- Static NuGet feed credentials also allow scheme-less URLs for request matching, but eager discovery currently enqueues the raw configured string as a discovery URL. That will fail request creation (missing protocol scheme) and skip discovery. For scheme-less sources, consider discovering both https:// and http:// variants (matching the earlier scheme-less behavior) while keeping explicit http/https sources distinct.
if !proxyOnly && url != "" {
logging.RequestLogf(nil, "fetching service index for nuget feed %s", url)
addNugetDiscoveryJob(&discoveryJobs, discoverySourceURLs, nugetDiscoveryJob{
serviceIndexURL: url,
staticCredential: feedCred,
| addNugetDiscoveryJob(&discoveryJobs, discoverySourceURLs, nugetDiscoveryJob{ | ||
| serviceIndexURL: url, | ||
| oidcCredential: oidcCredential, | ||
| }) |
|
this is an agent review comment guided by jeff
|
|
Noting for future reference.
|
|
this is an agent review comment guided by jeff Do not retain authentication across an HTTPS-to-HTTP redirect. The redirect callback currently copies |
This reverts commit c5795dd while preserving subsequent credential fallback and shared HTTP client behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run initial feed queries through a bounded worker pool and merge discovered credentials in configuration order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skip non-OK service index responses and safely handle empty or short response bodies during eager discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Deduplicate canonical credential URLs, preserve explicit credential precedence, and retain most-specific path matching with eager discovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adapt the reusable service-index scheme, deduplication, redirect, and response-handling tests from the reverted deferred-discovery implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1410ce1 to
0172569
Compare
| responseURL = redirectReq.URL | ||
| redirectAuthAllowed = redirectAuthAllowed && sameOrigin(via[len(via)-1].URL, redirectReq.URL) | ||
| if !redirectAuthAllowed { | ||
| redirectReq.Header.Del("Authorization") | ||
| redirectReq.Header.Del("X-Api-Key") | ||
| return nil | ||
| } | ||
| result.authenticatedRedirectURLs = append(result.authenticatedRedirectURLs, redirectReq.URL.String()) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Can we reject non-HTTPS redirects for OIDC discovery? IMO removing auth headers isn't enough here.
| req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, job.serviceIndexURL, nil) | ||
| if err != nil { | ||
| logging.RequestLogf(nil, "error creating http request (%s): %v", job.serviceIndexURL, err) | ||
| return nugetDiscoveryResult{} |
There was a problem hiding this comment.
Can you make sure to resolve schema-less URls e.g. feed.example.com/v3/index.json, to absolute URLs before http.NewRequestWithContext? Otherwise discovery fails and later successful index requests won't register credentials for the package endpoints.
There was a problem hiding this comment.
Are schema-less URIs common here? Technically they're not allowed in NuGet.Config so I'd prefer to reject them unless we have a reason to accept them.
| return nugetDiscoveryResult{} | ||
| } | ||
| } else { | ||
| authenticateNugetRequest(req, job.staticCredential, nil) |
There was a problem hiding this comment.
This bypasses normaliseHost, so the case-sensitive Azure check selects Bearer instead of Basic auth for a colonless PAT on PKGS.DEV.AZURE.COM.
|
In an effort to unblock some scenarios, this will be merged as-is and I'll immediately start work on all remaining comments here. |



Short description
Restore eager NuGet service-index discovery while retaining the useful credential matching and deduplication behavior introduced after the original implementation.
Paged v2 feeds and v3 service indexes can identify authenticated endpoints that differ from the configured repository URL. The proxy must discover those endpoints before the updater starts, otherwise requests can be made before the proxy knows which credentials to inject.
What are you trying to accomplish?
PR #203 changed NuGet discovery from eager initialization to deferred response interception. That caused update-job failures when authenticated resource URLs were needed before the proxy had observed the service-index response. This PR restores eager initialization and improves its performance, matching, determinism, and redirect handling.
The proxy still completes NuGet discovery before opening port 1080. Discovery requests now run through a bounded pool of eight workers, while results are merged in configured credential order.
Behavior before PR #203 compared with this PR
/feedcould match/feedback, and escaped separators could be conflated with literal separators. The first matching credential won.AuthorizationorX-Api-Keyacross redirects, including redirects to another host, and registers every followed redirect URL for later authenticated requests.Authentication formatting is unchanged: username/password values use Basic authentication, ordinary tokens use Bearer authentication, and Azure DevOps tokens are treated as Basic-auth passwords. Discovery still supports v2
xml:baseand non-template v3 resources.Anything you want to highlight for special attention from reviewers?
How will you know you've accomplished your goal?
go test ./...passes.Checklist