Repository navigation
fix(control-plane): return 503 for transient storage errors during DID resolution (#1087) - #1124
Merged
santoshkumarradha merged 2 commits intoOct 27, 2026
Conversation
…D resolution (Agent-Field#1087) ResolveDID collapsed any GetDIDDocument error into a notFound resolution result with a nil error, so a genuine missing DID and a transient DB failure were indistinguishable. Both flowed through VerifyDIDOwnership and the DID auth middleware as a 401. Since Agent-Field#1081 the Go SDK treats 401 as non-retryable, so a momentary storage blip now drops the status callback instead of being retried away. - storage: add ErrDIDDocumentNotFound sentinel and wrap sql.ErrNoRows with it in GetDIDDocument (message is unchanged, so no string-match breakage). - services: ResolveDID maps to notFound only for ErrDIDDocumentNotFound; any other storage error propagates wrapped in a new retryable ErrDIDResolutionUnavailable sentinel. IsDIDRevoked uses errors.Is against the same sentinel instead of brittle string matching. - middleware: return 503 (did_resolution_unavailable) when verification fails with ErrDIDResolutionUnavailable, otherwise keep the existing 401. Genuine not-found, revoked, invalid-document and signature-verification failures still return 401; only transient storage errors become 503. DID auth is off by default, so this only affects opt-in deployments. Tests: service-level (transient error propagates, genuine not-found stays notFound) and middleware-level (transient to 503, verification failure to 401). Updated two existing stubs to return the not-found sentinel so they mirror real storage.
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
santoshkumarradha
approved these changes
Oct 26, 2026
santoshkumarradha
left a comment
Member
There was a problem hiding this comment.
Reviewed the current head and focused validation. This looks good to me.
santoshkumarradha
enabled auto-merge
October 26, 2026 13:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
With DID auth enabled, a transient storage error while resolving a caller's DID was reported to the agent as
401 Unauthorized.DIDWebService.ResolveDIDcollapsed anyGetDIDDocumenterror into anotFoundresolution result with a nil error, so a genuine missing DID and a momentary DB failure were indistinguishable, and both surfaced as a 401. Since #1081 the Go SDK correctly treats 401 as non-retryable, so in this configuration a transient storage blip now drops the status callback instead of being retried away. This PR distinguishes "not found" from a storage error and returns503(retryable) for lookup failures.Type of change
What changed
storage: addedErrDIDDocumentNotFoundsentinel and wrappedsql.ErrNoRowswith it inGetDIDDocument. The error message is unchanged (DID document not found: <did>), so no string-matching callers break.services:ResolveDIDnow maps tonotFoundonly when the error isErrDIDDocumentNotFound; any other storage error propagates wrapped in a new retryableErrDIDResolutionUnavailablesentinel instead of being masked.IsDIDRevokednow useserrors.Isagainst the same sentinel rather than brittlestrings.Contains(..., "not found")matching.middleware: the DID auth middleware returns503(did_resolution_unavailable) when verification fails withErrDIDResolutionUnavailable, otherwise it keeps the existing401.Behavior that does not change
Genuine not-found, revoked, invalid-document, and signature-verification failures all still return
401. Only transient storage errors become503. DID auth is off by default, so this only affects deployments that opted in.Test plan
cd control-plane && go test ./internal/services/ -run DID -count=1cd control-plane && go test ./internal/server/middleware/ -run TestDIDAuth -count=1cd control-plane && go test ./internal/storage/ -count=1cd control-plane && go vet ./internal/services/ ./internal/server/middleware/ ./internal/storage/New tests:
GetDIDDocumenterror propagates asErrDIDResolutionUnavailable(not swallowed asnotFound), while a genuine not-found and the revoked/invalid cases keep their resolution metadata.503 did_resolution_unavailable; a plain verification failure still yields401 verification_error.ErrDIDDocumentNotFoundsentinel for genuinely absent DIDs so they mirror real storage.Two unrelated pre-existing failures in these packages (
TestGinLoggerEmitsStructuredDebugLine,TestNewSSRFSafeClient_AllowlistedLoopback) reproduce identically on a cleanmainand are not touched by this change.Test coverage
coverage-baseline.json... (n/a, no code removed)Checklist
docs/DEVELOPMENT.md.Related issues / PRs
Fixes #1087. Follow-up to #1081 (Go SDK status-callback retry policy).