1.24.3 — phonehub: a failed carrier lookup is negative-cached instead of crashing
Editorial identity incomplete
2026-09-09
A provider error on a never-cached phone number no longer raises IntegrityError and no longer re-bills the provider on every retry. The error is cached briefly with exponential backoff and exposed as lookup_unavailable; is_valid is never written by the error path, so a Twilio outage cannot mint a false verdict. Consumers that read PhoneNumber.is_valid must check lookup_unavailable first.
Changed
PhoneNumber.refresh()on a provider error now persists a short-lived negative cache:lookup_expires_atis stamped on every error atmin(15 × 2^(error_count−1), 1440)minutes, and the error is recorded inlookup_data(error,error_at,error_count; ANSI-stripped, truncated). Nothing else on the row moves — notis_valid, carrier fields,last_lookup_atorlookup_count.- New
PhoneNumber.lookup_unavailableproperty: true when the last attempt errored AND the row holds no successful lookup younger thanLOOKUP_TTL_DAYS. A previously-good number keeps serving its cached verdict through a short outage; a number that cannot be refreshed for longer than the TTL is treated as having no verdict.lookup_errorexposes the message. PhoneNumber.lookup()returnsNonefor a numbernormalize()cannot parse instead of attempting an insert with a nullphone_number.lookup_datais never serialized (NO_SHOW_FIELDS); thedefaultgraph exposeslookup_unavailablein its place. Module constantsLOOKUP_TTL_DAYS = 90andLOOKUP_ERROR_TTL_MINUTES = 15replace the literals.PhoneNumber.refresh()accepts a keyword-onlylookup_fntest seam; production callers pass nothing and behaviour is unchanged.
Fixed
refresh()raisedIntegrityError(lookup_expires_atNOT NULL) on every failed lookup of an uncached number, so the failure was never cached and the provider was billed again on each retry.POST /api/phonehub/number/lookupwithforce_refresh: trueand an unparseable number returned a 500; it now returns the documented{status: false}body.
Upgrade notes
is_validis the carrier verdict from the last SUCCESSFUL lookup. Any consumer that treats aPhoneNumberrow as a verdict must readlookup_unavailablefirst: a row that errored and has never succeeded still carries the field defaultis_valid=Truewith no carrier data. Deploy such consumers before or with this release; a consumer that readsis_validnaively will pass numbers whose lookup failed.- A Twilio "number not found" is still reported as a provider error, not as
is_valid=False; this release does not distinguish permanent from transient provider failures. - No migration.