From f078649ee45fc4b69c7e3d41ccff8b56020753f8 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Thu, 30 Jul 2026 19:36:33 +0200 Subject: [PATCH] appview/xrpc: sentence-case error messages and fix unclaimed-domain 500 The getDomainClaim handler returned 500 for the common no-claim case because scanClaim yields sql.ErrNoRows; treat that as an empty claim. Also sentence-case the user-facing error messages across the temp handlers (upper case, full stops). --- appview/xrpc/account.go | 10 +++++----- appview/xrpc/search.go | 8 ++++---- appview/xrpc/signup.go | 16 ++++++++-------- appview/xrpc/sites.go | 31 ++++++++++++++++--------------- appview/xrpc/webhooks.go | 18 +++++++++--------- appview/xrpc/xrpc.go | 6 +++--- 6 files changed, 45 insertions(+), 44 deletions(-) diff --git a/appview/xrpc/account.go b/appview/xrpc/account.go index 8aef97ee..520e25a9 100644 --- a/appview/xrpc/account.go +++ b/appview/xrpc/account.go @@ -61,7 +61,7 @@ func (x *Xrpc) AccountDeleteEmail(w http.ResponseWriter, r *http.Request) { existing, err := db.GetEmail(x.DB, did, addr) if err != nil { if errors.Is(err, sql.ErrNoRows) { - writeError(w, xrpcErrorTag("EmailNotFound", "the email address is not associated with this account"), http.StatusNotFound) + writeError(w, xrpcErrorTag("EmailNotFound", "The email address is not associated with this account."), http.StatusNotFound) return } l.Error("failed to get email", "err", err) @@ -69,7 +69,7 @@ func (x *Xrpc) AccountDeleteEmail(w http.ResponseWriter, r *http.Request) { return } if existing.Primary { - writeError(w, xrpcErrorTag("CannotDeletePrimary", "the primary email address cannot be deleted; set another address as primary first"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("CannotDeletePrimary", "The primary email address cannot be deleted; set another address as primary first."), http.StatusBadRequest) return } @@ -101,7 +101,7 @@ func (x *Xrpc) AccountSetPrimaryEmail(w http.ResponseWriter, r *http.Request) { existing, err := db.GetEmail(x.DB, did, addr) if err != nil { if errors.Is(err, sql.ErrNoRows) { - writeError(w, xrpcErrorTag("EmailNotFound", "the email address is not associated with this account"), http.StatusNotFound) + writeError(w, xrpcErrorTag("EmailNotFound", "The email address is not associated with this account."), http.StatusNotFound) return } l.Error("failed to get email", "err", err) @@ -109,7 +109,7 @@ func (x *Xrpc) AccountSetPrimaryEmail(w http.ResponseWriter, r *http.Request) { return } if !existing.Verified { - writeError(w, xrpcErrorTag("EmailNotVerified", "the email address must be verified before it can be made primary"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("EmailNotVerified", "The email address must be verified before it can be made primary."), http.StatusBadRequest) return } @@ -133,7 +133,7 @@ func (x *Xrpc) AccountSubscribeNewsletter(w http.ResponseWriter, r *http.Request primary, err := db.GetPrimaryEmail(x.DB, did) if err != nil || primary.Address == "" { - writeError(w, xrpcErrorTag("NoVerifiedEmail", "a primary email address is required to subscribe"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("NoVerifiedEmail", "A primary email address is required to subscribe."), http.StatusBadRequest) return } diff --git a/appview/xrpc/search.go b/appview/xrpc/search.go index a9cfe288..e2392848 100644 --- a/appview/xrpc/search.go +++ b/appview/xrpc/search.go @@ -18,14 +18,14 @@ func (x *Xrpc) SearchSearchCode(w http.ResponseWriter, r *http.Request) { l := x.Logger.With("handler", "SearchSearchCode") if x.CodeSearch == nil { - writeError(w, notImplementedError("code search is not configured"), http.StatusNotImplemented) + writeError(w, notImplementedError("Code search is not configured."), http.StatusNotImplemented) return } q := r.URL.Query() rawQuery := strings.TrimSpace(q.Get("q")) if rawQuery == "" { - writeError(w, badRequestError("missing required parameter: q"), http.StatusBadRequest) + writeError(w, badRequestError("Missing required parameter: q."), http.StatusBadRequest) return } @@ -35,7 +35,7 @@ func (x *Xrpc) SearchSearchCode(w http.ResponseWriter, r *http.Request) { if raw := strings.TrimSpace(q.Get("repoDid")); raw != "" { repoDid, err := syntax.ParseDID(raw) if err != nil { - writeError(w, badRequestError("invalid repoDid"), http.StatusBadRequest) + writeError(w, badRequestError("Invalid repoDid."), http.StatusBadRequest) return } repoFilter = repoDid @@ -62,7 +62,7 @@ func (x *Xrpc) SearchSearchCode(w http.ResponseWriter, r *http.Request) { if err != nil { var repoErr *codesearch.RepoOnlyError if errors.As(err, &repoErr) { - writeError(w, badRequestError("query only filters by repo name; use repo search instead"), http.StatusBadRequest) + writeError(w, badRequestError("Query only filters by repo name; use repo search instead."), http.StatusBadRequest) return } l.Error("code search failed", "err", err, "query", queryStr) diff --git a/appview/xrpc/signup.go b/appview/xrpc/signup.go index 07c39635..f8c4e86b 100644 --- a/appview/xrpc/signup.go +++ b/appview/xrpc/signup.go @@ -22,7 +22,7 @@ func (x *Xrpc) AccountBeginSignup(w http.ResponseWriter, r *http.Request) { // signup is gated on cloudflare being configured, mirroring appview/signup if x.Cloudflare == nil { - writeError(w, xrpcErrorTag("SignupDisabled", "signup is not currently enabled"), http.StatusFailedDependency) + writeError(w, xrpcErrorTag("SignupDisabled", "Signup is not currently enabled."), http.StatusFailedDependency) return } @@ -34,12 +34,12 @@ func (x *Xrpc) AccountBeginSignup(w http.ResponseWriter, r *http.Request) { if err := x.validateTurnstile(input.TurnstileToken, r); err != nil { l.Warn("turnstile validation failed", "err", err, "email", input.Email) - writeError(w, xrpcErrorTag("InvalidTurnstileToken", "captcha validation failed"), http.StatusForbidden) + writeError(w, xrpcErrorTag("InvalidTurnstileToken", "Captcha validation failed."), http.StatusForbidden) return } if !email.IsValidEmail(input.Email) { - writeError(w, xrpcErrorTag("InvalidEmail", "invalid email address"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidEmail", "Invalid email address."), http.StatusBadRequest) return } @@ -50,7 +50,7 @@ func (x *Xrpc) AccountBeginSignup(w http.ResponseWriter, r *http.Request) { return } if exists { - writeError(w, xrpcErrorTag("EmailAlreadyRegistered", "an account already exists for this email"), http.StatusConflict) + writeError(w, xrpcErrorTag("EmailAlreadyRegistered", "An account already exists for this email."), http.StatusConflict) return } @@ -89,7 +89,7 @@ func (x *Xrpc) AccountCompleteSignup(w http.ResponseWriter, r *http.Request) { l := x.Logger.With("handler", "AccountCompleteSignup") if x.Cloudflare == nil { - writeError(w, xrpcErrorTag("SignupDisabled", "signup is not currently enabled"), http.StatusFailedDependency) + writeError(w, xrpcErrorTag("SignupDisabled", "Signup is not currently enabled."), http.StatusFailedDependency) return } @@ -100,18 +100,18 @@ func (x *Xrpc) AccountCompleteSignup(w http.ResponseWriter, r *http.Request) { } if !userutil.IsValidSubdomain(input.Username) { - writeError(w, xrpcErrorTag("InvalidUsername", "invalid username"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidUsername", "Invalid username."), http.StatusBadRequest) return } if x.DisallowedNicknames[strings.ToLower(input.Username)] { - writeError(w, xrpcErrorTag("UsernameUnavailable", "this username is not available"), http.StatusConflict) + writeError(w, xrpcErrorTag("UsernameUnavailable", "This username is not available."), http.StatusConflict) return } emailAddr, err := db.GetEmailForCode(x.DB, input.Code) if err != nil { l.Error("failed to get email for code", "err", err) - writeError(w, xrpcErrorTag("InvalidCode", "invalid or expired verification code"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidCode", "Invalid or expired verification code."), http.StatusBadRequest) return } diff --git a/appview/xrpc/sites.go b/appview/xrpc/sites.go index 15210f39..c8b8aa85 100644 --- a/appview/xrpc/sites.go +++ b/appview/xrpc/sites.go @@ -2,6 +2,7 @@ package xrpc import ( "context" + "database/sql" "encoding/json" "errors" "net/http" @@ -27,7 +28,7 @@ func (x *Xrpc) SiteGetDomainClaim(w http.ResponseWriter, r *http.Request) { } claim, err := db.GetActiveDomainClaimForDid(x.DB, did) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { l.Error("failed to get domain claim", "err", err) writeError(w, errInternal, http.StatusInternalServerError) return @@ -57,21 +58,21 @@ func (x *Xrpc) SiteClaimDomain(w http.ResponseWriter, r *http.Request) { subdomain := strings.TrimSpace(input.Subdomain) if len(subdomain) < 4 { - writeError(w, xrpcErrorTag("InvalidSubdomain", "subdomain must be at least 4 characters long"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidSubdomain", "Subdomain must be at least 4 characters long."), http.StatusBadRequest) return } if !userutil.IsValidSubdomain(subdomain) { - writeError(w, xrpcErrorTag("InvalidSubdomain", "use only lowercase letters, digits, and hyphens; cannot start or end with a hyphen"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidSubdomain", "Use only lowercase letters, digits, and hyphens; cannot start or end with a hyphen."), http.StatusBadRequest) return } if userutil.HasSlur(subdomain) { - writeError(w, xrpcErrorTag("InvalidSubdomain", "that subdomain is not allowed"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidSubdomain", "That subdomain is not allowed."), http.StatusBadRequest) return } sitesDomain := x.Config.Sites.Domain if subdomain == sitesDomain { - writeError(w, xrpcErrorTag("InvalidSubdomain", "cannot claim the root domain"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("InvalidSubdomain", "Cannot claim the root domain."), http.StatusBadRequest) return } fullDomain := subdomain + "." + sitesDomain @@ -79,11 +80,11 @@ func (x *Xrpc) SiteClaimDomain(w http.ResponseWriter, r *http.Request) { if err := db.ClaimDomain(x.DB, did, fullDomain); err != nil { switch { case errors.Is(err, db.ErrDomainTaken): - writeError(w, xrpcErrorTag("DomainTaken", err.Error()), http.StatusConflict) + writeError(w, xrpcErrorTag("DomainTaken", "That subdomain is already claimed by another user."), http.StatusConflict) case errors.Is(err, db.ErrDomainCooldown): - writeError(w, xrpcErrorTag("DomainCooldown", err.Error()), http.StatusConflict) + writeError(w, xrpcErrorTag("DomainCooldown", "That subdomain was recently released and is in a cooldown period. Please try again later."), http.StatusConflict) case errors.Is(err, db.ErrAlreadyClaimed): - writeError(w, xrpcErrorTag("AlreadyClaimed", err.Error()), http.StatusConflict) + writeError(w, xrpcErrorTag("AlreadyClaimed", "You already have a domain claimed. Release it before claiming a new one."), http.StatusConflict) default: l.Error("claiming domain", "err", err) writeError(w, errInternal, http.StatusInternalServerError) @@ -111,7 +112,7 @@ func (x *Xrpc) SiteReleaseDomain(w http.ResponseWriter, r *http.Request) { domain := strings.TrimSpace(input.Domain) if domain == "" { - writeError(w, badRequestError("domain cannot be empty"), http.StatusBadRequest) + writeError(w, badRequestError("Domain cannot be empty."), http.StatusBadRequest) return } @@ -121,13 +122,13 @@ func (x *Xrpc) SiteReleaseDomain(w http.ResponseWriter, r *http.Request) { writeError(w, errInternal, http.StatusInternalServerError) return } else if isTngl { - writeError(w, xrpcErrorTag("HandleBoundDomain", "your tngl.sh domain is tied to your handle and cannot be released"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("HandleBoundDomain", "Your tngl.sh domain is tied to your handle and cannot be released."), http.StatusBadRequest) return } if err := db.ReleaseDomain(x.DB, did, domain); err != nil { l.Error("releasing domain", "err", err) - writeError(w, xrpcErrorTag("DomainNotFound", "unable to release domain; ensure it belongs to your account"), http.StatusNotFound) + writeError(w, xrpcErrorTag("DomainNotFound", "Unable to release domain; ensure it belongs to your account."), http.StatusNotFound) return } @@ -201,7 +202,7 @@ func (x *Xrpc) SiteUpdateRepoSiteConfig(w http.ResponseWriter, r *http.Request) branch := strings.TrimSpace(input.Branch) if branch == "" { - writeError(w, badRequestError("branch cannot be empty"), http.StatusBadRequest) + writeError(w, badRequestError("Branch cannot be empty."), http.StatusBadRequest) return } @@ -211,7 +212,7 @@ func (x *Xrpc) SiteUpdateRepoSiteConfig(w http.ResponseWriter, r *http.Request) } dir = path.Clean("/" + dir) if dir != "/" && strings.Contains(dir, "..") { - writeError(w, badRequestError("invalid directory path"), http.StatusBadRequest) + writeError(w, badRequestError("Invalid directory path."), http.StatusBadRequest) return } @@ -220,7 +221,7 @@ func (x *Xrpc) SiteUpdateRepoSiteConfig(w http.ResponseWriter, r *http.Request) // check the claim before persisting, so a failed call leaves no state ownerClaim, _ := db.GetActiveDomainClaimForDid(x.DB, repo.Did) if ownerClaim == nil { - writeError(w, xrpcErrorTag("NoDomainClaim", "the account does not have an active domain claim"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("NoDomainClaim", "The account does not have an active domain claim."), http.StatusBadRequest) return } @@ -256,7 +257,7 @@ func (x *Xrpc) SiteDisableRepoSite(w http.ResponseWriter, r *http.Request) { existingConfig, _ := db.GetRepoSiteConfig(x.DB, repo.RepoDid) if existingConfig == nil { - writeError(w, xrpcErrorTag("SiteNotFound", "no site configuration exists for this repository"), http.StatusNotFound) + writeError(w, xrpcErrorTag("SiteNotFound", "No site configuration exists for this repository."), http.StatusNotFound) return } diff --git a/appview/xrpc/webhooks.go b/appview/xrpc/webhooks.go index 50b9b40b..bdf1c8d5 100644 --- a/appview/xrpc/webhooks.go +++ b/appview/xrpc/webhooks.go @@ -25,7 +25,7 @@ func (x *Xrpc) resolveOwnedRepo(r *http.Request, repoDid string) (*models.Repo, repo, err := db.GetRepoByDid(x.DB, repoDid) if err != nil { - e := notFoundError("repo not found") + e := notFoundError("Repo not found.") return nil, &e, http.StatusNotFound } @@ -91,7 +91,7 @@ func (x *Xrpc) WebhookCreate(w http.ResponseWriter, r *http.Request) { return } if len(input.Events) == 0 { - writeError(w, xrpcErrorTag("NoEventsSelected", "at least one event must be specified"), http.StatusBadRequest) + writeError(w, xrpcErrorTag("NoEventsSelected", "At least one event must be specified."), http.StatusBadRequest) return } @@ -151,7 +151,7 @@ func (x *Xrpc) WebhookUpdate(w http.ResponseWriter, r *http.Request) { webhook, err := db.GetWebhook(x.DB, input.Id) if err != nil || string(webhook.RepoDid) != repo.RepoDid { - writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound) + writeError(w, xrpcErrorTag("WebhookNotFound", "Webhook not found."), http.StatusNotFound) return } @@ -214,7 +214,7 @@ func (x *Xrpc) WebhookDelete(w http.ResponseWriter, r *http.Request) { webhook, err := db.GetWebhook(x.DB, input.Id) if err != nil || string(webhook.RepoDid) != repo.RepoDid { - writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound) + writeError(w, xrpcErrorTag("WebhookNotFound", "Webhook not found."), http.StatusNotFound) return } @@ -257,7 +257,7 @@ func (x *Xrpc) WebhookToggle(w http.ResponseWriter, r *http.Request) { webhook, err := db.GetWebhook(x.DB, input.Id) if err != nil || string(webhook.RepoDid) != repo.RepoDid { - writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound) + writeError(w, xrpcErrorTag("WebhookNotFound", "Webhook not found."), http.StatusNotFound) return } @@ -297,13 +297,13 @@ func (x *Xrpc) WebhookListDeliveries(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(q.Get("id"), 10, 64) if err != nil { - writeError(w, badRequestError("invalid webhook id"), http.StatusBadRequest) + writeError(w, badRequestError("Invalid webhook ID."), http.StatusBadRequest) return } webhook, err := db.GetWebhook(x.DB, id) if err != nil || string(webhook.RepoDid) != repo.RepoDid { - writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound) + writeError(w, xrpcErrorTag("WebhookNotFound", "Webhook not found."), http.StatusNotFound) return } @@ -365,13 +365,13 @@ func (x *Xrpc) WebhookRetryDelivery(w http.ResponseWriter, r *http.Request) { webhook, err := db.GetWebhook(x.DB, input.WebhookId) if err != nil || string(webhook.RepoDid) != repo.RepoDid { - writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound) + writeError(w, xrpcErrorTag("WebhookNotFound", "Webhook not found."), http.StatusNotFound) return } delivery, err := db.GetWebhookDelivery(x.DB, input.DeliveryId) if err != nil || delivery.WebhookId != webhook.Id { - writeError(w, xrpcErrorTag("DeliveryNotFound", "delivery not found"), http.StatusNotFound) + writeError(w, xrpcErrorTag("DeliveryNotFound", "Delivery not found."), http.StatusNotFound) return } diff --git a/appview/xrpc/xrpc.go b/appview/xrpc/xrpc.go index 8c358e31..4a88937e 100644 --- a/appview/xrpc/xrpc.go +++ b/appview/xrpc/xrpc.go @@ -162,9 +162,9 @@ func actorDid(r *http.Request) (string, bool) { // stable client-facing errors; handlers log the real cause and return these var ( - errInternal = xrpcErrorTag("InternalError", "internal server error") - errBadRequestBody = xrpcErrorTag("InvalidRequest", "invalid request body") - errUpstream = xrpcErrorTag("UpstreamError", "an upstream service failed") + errInternal = xrpcErrorTag("InternalError", "Internal server error.") + errBadRequestBody = xrpcErrorTag("InvalidRequest", "Invalid request body.") + errUpstream = xrpcErrorTag("UpstreamError", "An upstream service failed.") ) func xrpcErrorTag(tag, message string) xrpcerr.XrpcError { -- 2.51.2