Skip to content

fix(control-plane): return 503 for transient storage errors during DID resolution (#1087) - #1124

Merged
santoshkumarradha merged 2 commits into
Agent-Field:mainfrom
7vignesh:fix/1087-did-auth-503-transient-storage
Oct 27, 2026
Merged

santoshkumarradha merged 2 commits into
Agent-Field:mainfrom
7vignesh:fix/1087-did-auth-503-transient-storage

Conversation

@7vignesh

Copy link
Copy Markdown
Contributor

Summary

With DID auth enabled, a transient storage error while resolving a caller's DID was reported to the agent as 401 Unauthorized. DIDWebService.ResolveDID collapsed any GetDIDDocument error into a notFound resolution 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 returns 503 (retryable) for lookup failures.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Docs only
  • Tests only
  • CI / tooling
  • Breaking change

What changed

  • storage: added ErrDIDDocumentNotFound sentinel and wrapped sql.ErrNoRows with it in GetDIDDocument. The error message is unchanged (DID document not found: <did>), so no string-matching callers break.
  • services: ResolveDID now maps to notFound only when the error is ErrDIDDocumentNotFound; any other storage error propagates wrapped in a new retryable ErrDIDResolutionUnavailable sentinel instead of being masked. IsDIDRevoked now uses errors.Is against the same sentinel rather than brittle strings.Contains(..., "not found") matching.
  • middleware: the DID auth middleware returns 503 (did_resolution_unavailable) when verification fails with ErrDIDResolutionUnavailable, otherwise it keeps the existing 401.

Behavior that does not change

Genuine not-found, revoked, invalid-document, and signature-verification failures all still return 401. Only transient storage errors become 503. 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=1
  • cd control-plane && go test ./internal/server/middleware/ -run TestDIDAuth -count=1
  • cd control-plane && go test ./internal/storage/ -count=1
  • cd control-plane && go vet ./internal/services/ ./internal/server/middleware/ ./internal/storage/

New tests:

  • Service level: a transient GetDIDDocument error propagates as ErrDIDResolutionUnavailable (not swallowed as notFound), while a genuine not-found and the revoked/invalid cases keep their resolution metadata.
  • Middleware level: a transient resolution error yields 503 did_resolution_unavailable; a plain verification failure still yields 401 verification_error.
  • Two existing stubs were updated to return the ErrDIDDocumentNotFound sentinel for genuinely absent DIDs so they mirror real storage.

Two unrelated pre-existing failures in these packages (TestGinLoggerEmitsStructuredDebugLine, TestNewSSRFSafeClient_AllowlistedLoopback) reproduce identically on a clean main and are not touched by this change.

Test coverage

  • I ran tests for the surface(s) I changed locally.
  • New code paths are covered by tests in this PR (no bare additions).
  • If I removed code, I updated coverage-baseline.json ... (n/a, no code removed)
  • The coverage gate check is green in CI before requesting review.

Checklist

  • I have read docs/DEVELOPMENT.md.
  • Commits are signed and follow conventional-commits style. (conventional-commits style used; commit is unsigned - no GPG key configured in this environment.)
  • I have linked any related issues (see below).

Related issues / PRs

Fixes #1087. Follow-up to #1081 (Go SDK status-callback retry policy).

…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.
@7vignesh
7vignesh requested review from a team and AbirAbbas as code owners October 20, 2026 21:22
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 88.20% 87.40% ↑ +0.80 pp 🟡
sdk-go 93.70% 92.00% ↑ +1.70 pp 🟢
sdk-python 94.72% 93.73% ↑ +0.99 pp 🟢
sdk-typescript 92.50% 90.42% ↑ +2.08 pp 🟢
web-ui 84.89% 84.79% ↑ +0.10 pp 🟡
aggregate 86.11% 85.75% ↑ +0.36 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 23 95.00% ✅
sdk-go 0 — ➖ no changes
sdk-python 0 — ➖ no changes
sdk-typescript 0 — ➖ no changes
web-ui 0 — ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the current head and focused validation. This looks good to me.

@santoshkumarradha
santoshkumarradha added this pull request to the merge queue Oct 27, 2026
Merged via the queue into Agent-Field:main with commit 7306a0e Oct 27, 2026
21 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.

DID auth returns 401 for transient storage errors during did:web resolution

2 participants