fix(registry): surface legacy registry format as a structured API error#5260
Conversation
Before this change, when a configured custom registry URL served data in the legacy ToolHive format, NewRemoteRegistryProvider returned a plain fmt.Errorf that the API handler did not recognise. The API responded with a generic HTTP 500 "Failed to get registry provider", and the actionable migration hint was visible only in main.log. Desktop clients had no way to distinguish this case from an internal error. Add a typed *LegacyFormatError (sister of *UnavailableError) carrying the offending source URL. Wire it through the four registry GET handlers and the v0.1 router so the response is now HTTP 503 with a structured "registry_legacy_format" code and the migration hint in the message body. The upstream_parser sentinel errLegacyFormat is now an instance of *LegacyFormatError; its Is() method preserves errors.Is(err, errLegacyFormat) for existing callers while enabling errors.As extraction of the URL. Fixes #5259 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
503 was inconsistent with the existing PUT registry handler that already returns 502 for the same legacy-format detection via ErrRegistryValidationFailed (pkg/api/v1/registry.go updateRegistry handler). 502 is also the correct RFC 9110 §15.6.3 code: thv serve acts as a gateway to the upstream registry and the upstream returned a response we cannot process. 503 is reserved for "temporary overload or scheduled maintenance" which does not match a misconfigured registry source. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5260 +/- ##
==========================================
+ Coverage 68.02% 68.04% +0.02%
==========================================
Files 616 616
Lines 63005 63044 +39
==========================================
+ Hits 42857 42900 +43
+ Misses 16945 16940 -5
- Partials 3203 3204 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR improves API ergonomics when a configured custom registry URL serves legacy ToolHive registry JSON by returning a structured, machine-detectable error (code: "registry_legacy_format") with HTTP 502 Bad Gateway, instead of a generic 500 that only logs the migration hint.
Changes:
- Introduces a typed
*registry.LegacyFormatError(withIs()support) and uses it for legacy-format detection in both parsing and remote connectivity validation. - Adds API-layer
errors.Ashandling and a dedicated JSON error writer to consistently return 502 + structured error code across registry GET paths (including v0.1 router). - Adds/extends unit and API tests to validate
Is/Asbehavior and the 502 structured response.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/registry/upstream_parser.go | Switches legacy-format sentinel to *LegacyFormatError for errors.As support. |
| pkg/registry/provider_remote.go | Returns *LegacyFormatError{URL: ...} from connectivity validation on legacy data. |
| pkg/registry/errors.go | Adds LegacyFormatError type (sibling to UnavailableError). |
| pkg/registry/errors_test.go | Adds tests for LegacyFormatError Error(), Is, and As. |
| pkg/api/v1/registry.go | Adds registry_legacy_format code + 502 writer and wires errors.As handling into GET handlers. |
| pkg/api/v1/registry_v01.go | Adds legacy-format error handling to v0.1 provider resolution. |
| pkg/api/v1/registry_test.go | Adds end-to-end coverage asserting 502 + structured legacy error code/message for GET endpoints. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two doc comments missed the 502 update from the previous commit. The code itself already returns 502 Bad Gateway via writeRegistryLegacyFormatError; only the comments were inconsistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude succinct review |
|
Claude finished @peppescg's task in 1m 33s —— View job Succinct reviewOverall: clean, well-scoped, and the 502 rationale in the PR body is solid. Two minor suggestions, plus a coverage note. Status of prior feedback
🟢 Strengths
🟡 Suggestions (non-blocking)
Nits
LGTM with the above. The helper extraction in suggestion #1 would be the highest-value follow-up; either ship in this PR or as a quick sequel. |
The same `errors.As` ladder for `*LegacyFormatError`, `*UnavailableError`, plus the `auth.ErrRegistryAuthRequired` check was copy-pasted across `getCurrentProvider`, `listRegistries`, `getRegistry`, `listServers`, and `getRegistryProvider`. Each new error code that needs structured handling had to be wired into five places, and the existing v0.1 router did the same `errors.Is` check inline instead of going through `isRegistryAuthError`. Extract a single `writeProviderError(w, err) bool` helper that handles all three known failure modes and returns whether a response was written. Each callsite collapses to two lines and a generic fallback. Drop the now-unused `errors` and `registry/auth` imports from `registry_v01.go`. Also lower the legacy-format log to `slog.Warn`: it signals a user misconfiguration that the API has already translated into a 502 with an actionable hint, not a server-side failure that on-call should investigate. The upstream-unavailable path stays at `slog.Error` since it usually does indicate a real outage. Improves on #5260
The same `errors.As` ladder for `*LegacyFormatError`, `*UnavailableError`, plus the `auth.ErrRegistryAuthRequired` check was copy-pasted across `getCurrentProvider`, `listRegistries`, `getRegistry`, `listServers`, and `getRegistryProvider`. Each new error code that needs structured handling had to be wired into five places, and the existing v0.1 router did the same `errors.Is` check inline instead of going through `isRegistryAuthError`. Extract a single `writeProviderError(w, err) bool` helper that handles all three known failure modes and returns whether a response was written. Each callsite collapses to two lines and a generic fallback. Drop the now-unused `errors` and `registry/auth` imports from `registry_v01.go`. Also lower the legacy-format log to `slog.Warn`: it signals a user misconfiguration that the API has already translated into a 502 with an actionable hint, not a server-side failure that on-call should investigate. The upstream-unavailable path stays at `slog.Error` since it usually does indicate a real outage. Improves on #5260
* feat(registry): surface legacy registry format with targeted UI The backend (stacklok/toolhive#5260) now returns a structured HTTP 502 Bad Gateway with `code: "registry_legacy_format"` when the configured custom registry serves data in the deprecated ToolHive format. Without renderer-side handling the response was treated as a generic fetch error and the user fell into the catch-all GenericError screen. This change mirrors the existing `registry_auth_required` and `registry_unavailable` plumbing: - Add `isRegistryLegacyFormatError` detector plus the user-facing `REGISTRY_LEGACY_FORMAT_UI_MESSAGE` and toast copy (no raw CLI hint — desktop users should not see `thv registry convert`). - Extend `getRegistryErrorToastMessage` so the root `beforeLoad` guard redirects to `/settings?tab=registry` with a persistent toast instead of letting the error reach `RootErrorComponent`. - Add a third empty-state branch in `RegistryError` for users who navigate directly to `/registry` while the source is misconfigured. Closes #2228 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(registry): drop CLI/MCP jargon from legacy-format UI copy REGISTRY_LEGACY_FORMAT_UI_MESSAGE referred to the "upstream MCP format", which is a CLI/protocol term that desktop users would not recognise. Rephrase in plain language: the message now says to update the registry source or reset to default, without naming the destination format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
When a configured custom registry URL serves data in the legacy ToolHive format, the API now returns HTTP 502 Bad Gateway with a structured
registry_legacy_formatcode, instead of a generic 500 that hides the actionable migration hint in the log.Before,
NewRemoteRegistryProvider.validateConnectivity()returned a plainfmt.Errorfthat the API handler did not recognise (pkg/api/v1/registry.go:259), so the response wasHTTP 500 Failed to get registry providerand clients had no way to branch on the legacy-format case. The hint (run thv registry convert --in <path> --in-place) only showed up inmain.log.This PR adds a typed
*LegacyFormatErrorinpkg/registry/errors.go(sister of*UnavailableError) that carries the offending source URL, and wireserrors.Asdetection plus a newwriteRegistryLegacyFormatErrorwriter through all fourGEThandlers and thev0.1router.The package-private
errLegacyFormatsentinel inpkg/registry/upstream_parser.gois now an instance of*LegacyFormatError; itsIs()method preserveserrors.Is(err, errLegacyFormat)for existing callers while enablingerrors.Asextraction of the URL upstream.Closes #5259
Why HTTP 502 Bad Gateway
Per RFC 9110 §15.6.3: 502 = "while acting as a gateway or proxy, received an invalid response from an inbound server."
thv serveis acting as a gateway to the configured upstream registry, and that upstream is returning data we cannot process. Exact fit.502 also matches what the existing PUT registry handler already does for the same legacy-format detection (
pkg/api/v1/registry.go:545, viaErrRegistryValidationFailed). Returning 503 fromGETwould split the same condition across two status codes inside the same file.503 ("temporary overload or scheduled maintenance") does not fit a permanently misconfigured registry source.
The
registry_unavailablepath also handles upstream 404s on a 503 today — arguably also 502 territory — but reclassifying that is out of scope for this PR.Changes
pkg/registry/errors.go— newLegacyFormatErrortype withURL,Error(),Is().pkg/registry/provider_remote.go—validateConnectivity()returns&LegacyFormatError{URL: p.registryURL}instead offmt.Errorf.pkg/registry/upstream_parser.go—errLegacyFormatswitches to&LegacyFormatError{}(sameError()output, now typed forerrors.As).pkg/api/v1/registry.go— newRegistryLegacyFormatCode+writeRegistryLegacyFormatErrorreturning 502; newerrors.As(err, &legacyErr)branch ingetCurrentProvider,listRegistries,getRegistry,listServers.pkg/api/v1/registry_v01.go— same branch ingetRegistryProvider.pkg/registry/errors_test.go— coversError()with/without URL,Ismatching across populated/empty URLs and viaUnwrap, andAsextraction from a wrapped error.pkg/api/v1/registry_test.go— end-to-end test: configure a remote-URL registry pointing at an httptest server that serves legacy JSON, hit eachGEThandler, assert 502 +code: registry_legacy_format+ migration hint in the message + source URL in the message.Test plan
go test ./pkg/registry/ -run "TestLegacyFormatError" -count=1— new error type tests passgo test ./pkg/api/v1/ -run "TestRegistryAPI_GetEndpoint_LegacyFormat" -count=1— all three GET handlers return 502 with the new codego test ./pkg/registry/ -run "TestParseRegistryData_LegacyFormatDetection|TestLocalRegistryProvider_LegacyFileReturnsMigrationHint|TestRemoteRegistryProvider_ValidateConnectivity" -count=1— existing legacy-detection coverage still greengo vet ./pkg/registry/... ./pkg/api/v1/...cleangofmt -lcleanFour pre-existing failures on
main(TestGetDefaultProvider_NoFactoryRegistered,TestGetDefaultProvider_FactoryReturnsNil_FallsThrough,TestRegistryV01Router_GetServer_NotFound,TestRegistryV01Router_ListSkills_PaginationBeyondResults) reproduce ongit stashof this branch — unrelated to this change.🤖 Generated with Claude Code