Skip to content

Commit 2b7c319

Browse files
committed
fix: fail fast on transient DB errors in custom domain verification
https://linear.app/speakeasy/issue/AGE-1630/bug-do-not-create-domains-on-transient-lookup-errors Previously, any error from `GetCustomDomainByDomain` triggered domain creation. Now only `pgx.ErrNoRows` does — other errors (e.g. connection failures) return immediately instead of creating spurious domain records. Also extracts DNS lookups into a `dns.Resolver` interface to enable unit testing, and adds 13 tests covering domain creation, error handling, DNS verification, and the transient error fix.
1 parent c9d23f8 commit 2b7c319

File tree

7 files changed

+414
-5
lines changed

7 files changed

+414
-5
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"server": patch
3+
---
4+
5+
Fix custom domain verification to fail fast on transient database errors instead of incorrectly creating a new domain record

server/internal/background/activities/verify_custom_domain.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8-
"net"
98
"regexp"
109
"slices"
1110
"strings"
1211

12+
"github.com/jackc/pgx/v5"
1313
"github.com/jackc/pgx/v5/pgxpool"
1414

1515
"github.com/speakeasy-api/gram/server/internal/attr"
1616
"github.com/speakeasy-api/gram/server/internal/audit"
1717
"github.com/speakeasy-api/gram/server/internal/conv"
1818
customdomainsRepo "github.com/speakeasy-api/gram/server/internal/customdomains/repo"
19+
"github.com/speakeasy-api/gram/server/internal/dns"
1920
"github.com/speakeasy-api/gram/server/internal/o11y"
2021
"github.com/speakeasy-api/gram/server/internal/oops"
2122
"github.com/speakeasy-api/gram/server/internal/urn"
@@ -25,16 +26,23 @@ type VerifyCustomDomain struct {
2526
db *pgxpool.Pool
2627
logger *slog.Logger
2728
expectedTargetCNAME string
29+
resolver dns.Resolver
2830
}
2931

3032
func NewVerifyCustomDomain(logger *slog.Logger, db *pgxpool.Pool, expectedTargetCNAME string) *VerifyCustomDomain {
3133
return &VerifyCustomDomain{
3234
db: db,
3335
logger: logger,
3436
expectedTargetCNAME: expectedTargetCNAME,
37+
resolver: dns.NetResolver{},
3538
}
3639
}
3740

41+
// SetResolver replaces the DNS resolver. Intended for testing.
42+
func (d *VerifyCustomDomain) SetResolver(r dns.Resolver) {
43+
d.resolver = r
44+
}
45+
3846
type VerifyCustomDomainArgs struct {
3947
OrgID string
4048
Domain string
@@ -66,7 +74,10 @@ func (d *VerifyCustomDomain) Do(ctx context.Context, args VerifyCustomDomainArgs
6674
cdr := customdomainsRepo.New(dbtx)
6775

6876
domain, err := cdr.GetCustomDomainByDomain(ctx, args.Domain)
69-
if err != nil {
77+
switch {
78+
case err == nil:
79+
// Domain already exists, continue
80+
case errors.Is(err, pgx.ErrNoRows):
7081
// Create a new unverified domain entry
7182
domain, err = cdr.CreateCustomDomain(ctx, customdomainsRepo.CreateCustomDomainParams{
7283
OrganizationID: args.OrgID,
@@ -88,6 +99,8 @@ func (d *VerifyCustomDomain) Do(ctx context.Context, args VerifyCustomDomainArgs
8899
}); err != nil {
89100
return oops.E(oops.CodeUnexpected, err, "failed to create custom domain creation audit log").Log(ctx, d.logger)
90101
}
102+
default:
103+
return oops.E(oops.CodeUnexpected, err, "failed to get custom domain").Log(ctx, d.logger)
91104
}
92105

93106
if err := dbtx.Commit(ctx); err != nil {
@@ -98,11 +111,11 @@ func (d *VerifyCustomDomain) Do(ctx context.Context, args VerifyCustomDomainArgs
98111
return oops.E(oops.CodeUnauthorized, errors.New("custom domain does not belong to organization"), "custom domain does not belong to organization").Log(ctx, d.logger)
99112
}
100113

101-
cname, err := net.LookupCNAME(domain.Domain)
114+
cname, err := d.resolver.LookupCNAME(domain.Domain)
102115
if err != nil {
103116
d.logger.InfoContext(ctx, "CNAME lookup failed for domain", attr.SlogURLDomain(domain.Domain), attr.SlogError(err))
104117
// Provide more info if an A record exists
105-
ips, aErr := net.LookupHost(domain.Domain)
118+
ips, aErr := d.resolver.LookupHost(domain.Domain)
106119
if aErr == nil && len(ips) > 0 {
107120
d.logger.InfoContext(ctx, fmt.Sprintf("CNAME not found. Found A record(s): %s", strings.Join(ips, ", ")))
108121
} else {
@@ -117,7 +130,7 @@ func (d *VerifyCustomDomain) Do(ctx context.Context, args VerifyCustomDomainArgs
117130
}
118131

119132
txtName := "_gram." + domain.Domain
120-
txts, err := net.LookupTXT(txtName)
133+
txts, err := d.resolver.LookupTXT(txtName)
121134
if err != nil {
122135
return oops.E(oops.CodeUnexpected, err, "failed to find TXT record for %s", txtName).Log(ctx, d.logger)
123136
}

0 commit comments

Comments
 (0)