From c7fa9bd5887666e12b555396b5ce64b8af2b3c5f Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sat, 28 Feb 2026 04:15:23 -0800 Subject: fix(api): align tombstoned DID compatibility with plc.directory --- internal/api/plc_compatibility_test.go | 50 ++++++++++++++++++ internal/api/server.go | 96 +++++++++++++++++++--------------- 2 files changed, 105 insertions(+), 41 deletions(-) diff --git a/internal/api/plc_compatibility_test.go b/internal/api/plc_compatibility_test.go index 45428d8..a669f0c 100644 --- a/internal/api/plc_compatibility_test.go +++ b/internal/api/plc_compatibility_test.go @@ -302,6 +302,56 @@ func TestPLCCompatibilityNotFoundUsesPLCErrorShape(t *testing.T) { } } +func TestPLCCompatibilityTombstonedDIDReturnsNotAvailable404(t *testing.T) { + ts, store, _, cleanup := newCompatibilityServer(t) + defer cleanup() + + const tombDID = "did:plc:tombstone-test" + + if err := store.PutState(types.StateV1{ + DID: tombDID, + ChainTipHash: "bafy-tomb-tip", + DIDDocument: []byte(`{"id":"did:plc:tombstone-test","deactivated":true}`), + }); err != nil { + t.Fatalf("put tombstoned state: %v", err) + } + + check := func(path string) { + t.Helper() + + resp, err := http.Get(ts.URL + path) + if err != nil { + t.Fatalf("get %s: %v", path, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("%s status: got %d want 404", path, resp.StatusCode) + } + + if got := resp.Header.Get("Content-Type"); got != "application/json; charset=utf-8" { + t.Fatalf("%s content-type mismatch: got %q want %q", path, got, "application/json; charset=utf-8") + } + + var body map[string]any + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("%s decode body: %v", path, err) + } + + if len(body) != 1 { + t.Fatalf("%s expected single message field, got: %v", path, body) + } + + want := "DID not available: " + tombDID + if got, _ := body["message"].(string); got != want { + t.Fatalf("%s message mismatch: got %q want %q", path, got, want) + } + } + + check("/" + tombDID) + check("/" + tombDID + "/data") +} + func newCompatibilityServer(t *testing.T) (*httptest.Server, *storage.PebbleStore, []types.ExportRecord, func()) { t.Helper() diff --git a/internal/api/server.go b/internal/api/server.go index 9895a65..284c2b2 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -479,43 +479,15 @@ func (s *Server) handlePLCCompatibility(w http.ResponseWriter, r *http.Request) } func (s *Server) handleGetDIDCompatibility(w http.ResponseWriter, r *http.Request, did string) { - var state types.StateV1 - if s.ingestor != nil { - resolvedState, err := s.ingestor.ResolveState(r.Context(), did) - if err != nil { - if errors.Is(err, ingest.ErrDIDNotFound) { - writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) - - return - } - - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) - - return - } - - writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) - - return - } - - state = resolvedState - } else { - stateVal, ok, err := s.store.GetState(did) - if err != nil { - writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) - - return - } - - if !ok { - writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) + state, ok := s.resolveCompatibilityState(w, r, did) + if !ok { + return + } - return - } + if isTombstonedDIDDocument(state.DIDDocument) { + writeCompatibilityErr(w, http.StatusNotFound, "DID not available: "+did) - state = stateVal + return } data, err := s.ingestor.LoadCurrentPLCData(r.Context(), did) @@ -539,11 +511,7 @@ func (s *Server) handleGetDIDCompatibility(w http.ResponseWriter, r *http.Reques } status := http.StatusOK - deactivated := isTombstonedDIDDocument(state.DIDDocument) - - if deactivated { - status = http.StatusGone - } + deactivated := false writeJSONWithContentType(w, status, "application/did+ld+json", buildPLCDIDDocument(did, data, deactivated)) } @@ -666,6 +634,17 @@ func (s *Server) handleGetDIDDataCompatibility(w http.ResponseWriter, r *http.Re return } + state, ok := s.resolveCompatibilityState(w, r, did) + if !ok { + return + } + + if isTombstonedDIDDocument(state.DIDDocument) { + writeCompatibilityErr(w, http.StatusNotFound, "DID not available: "+did) + + return + } + data, err := s.ingestor.LoadCurrentPLCData(r.Context(), did) if err != nil { @@ -689,6 +668,41 @@ func (s *Server) handleGetDIDDataCompatibility(w http.ResponseWriter, r *http.Re writeJSONWithContentType(w, http.StatusOK, "application/json", data) } +func (s *Server) resolveCompatibilityState(w http.ResponseWriter, r *http.Request, did string) (types.StateV1, bool) { + if s.ingestor != nil { + state, err := s.ingestor.ResolveState(r.Context(), did) + if err != nil { + if errors.Is(err, ingest.ErrDIDNotFound) { + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) + return types.StateV1{}, false + } + + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + writeCompatibilityErr(w, http.StatusGatewayTimeout, err.Error()) + return types.StateV1{}, false + } + + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) + return types.StateV1{}, false + } + + return state, true + } + + stateVal, found, err := s.store.GetState(did) + if err != nil { + writeCompatibilityErr(w, http.StatusInternalServerError, err.Error()) + return types.StateV1{}, false + } + + if !found { + writeCompatibilityErr(w, http.StatusNotFound, "DID not registered: "+did) + return types.StateV1{}, false + } + + return stateVal, true +} + func (s *Server) handleExportCompatibility(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.Header().Set("Allow", http.MethodGet) @@ -964,7 +978,7 @@ func extractServicesMap(v any) map[string]map[string]string { } func writeCompatibilityErr(w http.ResponseWriter, code int, message string) { - writeJSON(w, code, map[string]any{"message": message}) + writeJSONWithContentType(w, code, "application/json; charset=utf-8", map[string]any{"message": message}) } func (s *Server) withTimeout(next http.Handler) http.Handler { -- cgit v1.2.3