fix: preserve the underlying cause in LookupError - #202
Open
NeoCN wants to merge 1 commit into
Open
Conversation
NeoCN
force-pushed
the
fix/preserve-smtp-error-cause
branch
from
August 25, 2026 09:51
b978005 to
2be9b52
Compare
ParseSMTPError replaced the error it was given with one of a fixed set of
canned messages, discarding the original. Callers could only recover the
reason by substring-matching Details, and could not reach the underlying
*net.DNSError, *net.OpError or *textproto.Error at all.
LookupError now records the error it was derived from and exposes it via
Unwrap, so errors.Is and errors.As work. The cause is unexported and not
serialised, so the JSON and XML shapes are unchanged.
Also stops ParseSMTPError returning a nil *LookupError. It did so whenever the
status line did not itself indicate a failure, but it is only ever reached with
a non-nil error, so that discarded a real failure -- and callers doing
return &ret, ParseSMTPError(err)
were handed a non-nil error interface wrapping a nil pointer, whose Error
method panics on the receiver. cmd/apiserver calls err.Error() on exactly that
path. An error we cannot classify is now reported verbatim instead.
TestParseError_Code400_Nil asserted the nil return, so it is replaced by
TestParseError_Code400_ReportedVerbatim. Three tests that compared a whole
LookupError with assert.Equal now assert on Message and Details, matching the
rest of the file and staying robust to added fields.
Coverage 89.6% -> 89.7%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NeoCN
force-pushed
the
fix/preserve-smtp-error-cause
branch
from
August 25, 2026 10:15
2be9b52 to
86e79c4
Compare
mzcaptainyang
approved these changes
Aug 25, 2026
hodge1997
approved these changes
Aug 26, 2026
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.
ParseSMTPErrorreplaced the error it was handed with one of a fixed set of canned messages and dropped the original. Callers could only recover the reason by substring-matchingDetails, and could not reach the underlying*net.DNSError,*net.OpErroror*textproto.Errorat all.Wrapping instead of replacing
LookupErrornow records the error it was derived from and exposes it viaUnwrap, soerrors.Isanderrors.Aswork:The field is unexported and not serialised, so the JSON and XML shapes are unchanged.
Fixing a nil that callers cannot handle
ParseSMTPErrorreturned a nil*LookupErrorwhenever the status line did not itself indicate a failure. It is only ever reached with a non-nil error, so that discarded a real failure — and because the result is returned through anerrorinterface:callers were handed a non-nil interface wrapping a nil pointer.
LookupError.Errordereferences its receiver, so calling it on that panics.cmd/apiserverdoes exactly that, on two lines.An error that cannot be classified is now reported verbatim, so a non-nil input always yields a non-nil result. The classification logic itself is unchanged, just moved into an internal function behind a thin exported wrapper.
Compatibility
This is a breaking change for downstream code, in three ways. None of them affect the JSON or XML output.
ParseSMTPErrornever returns nil for a non-nil input. A caller writingif e := ParseSMTPError(err); e != nil { ... }now enters that branch for a reply whose status parses to 400 or below, where previously it did not, ande.Messagethere is the raw error string rather than one of the exportedErr*constants. Code that assumesMessageis always one of those constants will fall through to whatever it does by default. This repo's own use of that pattern, inCheckSMTP's catch-all switch, lands on an emptydefault:and is unaffected — but external code need not be so lucky.LookupErrorgained an unexported field. An external unkeyed literal —emailverifier.LookupError{"msg", "details"}— no longer compiles, and cannot be fixed by adding a third value, since Go forbids supplying a value for another package's unexported field.go vetalready discourages unkeyed literals for other packages' structs, but the break is real. The constructor in this package had to change for the same reason.Whole-struct comparison no longer matches.
assert.Equalandreflect.DeepEqualinspect unexported fields, and the cause is populated on every return path, so comparing a result against a hand-built&LookupError{Message: ..., Details: ...}now fails. Three tests in this PR had to change for exactly this reason, and any downstream test written the same way will too. ComparingMessageandDetailsindividually is the fix.Tagged
**Breaking**in the changelog, alongside #198.Tests
TestParseError_UnclassifiedIsReportedVerbatimcovers the new branch across200 OK,300 Redirect,399and400, asserting the verbatim message, thatError()does not panic, and that the cause survives. That last assertion matters: with only the previous single-value test, removing.withCause(err)from this branch passed the entire suite.Also covers a nil input,
errors.Asreaching the cause on the classified branch, andUnwrapon a nil receiver. Each was checked by reverting the corresponding production change and confirming the test fails.TestParseError_Code400_Nilasserted the old nil return and is gone, replaced by the table above. Three tests that compared a wholeLookupErrornow assert onMessageandDetails, matching the rest of the file.Scope
Groundwork, not a behaviour change to classification: every reply that produced a given
Messagebefore still produces it.Distinguishing rejections that concern the sender from those that concern the recipient — what #152, #126 and #72 need — means changing which
Messagea reply maps to, and theswitchinCheckSMTPcases on exactly three of those values, so any remapping silently reroutes a reply to the emptydefaultand flipsCatchAll. Doing that safely needs deterministic tests for the 4xx, greylisting and blocked paths, which the live-network suite cannot produce. Left for a later change.🤖 Generated with Claude Code