fix(labrinth): let through emails when neverbounce hangs or fails (#6898)

* fix(labrinth): let through emails when neverbounce hangs or fails

* chore(ci): stack size
This commit is contained in:
François-Xavier Talbot
2026-07-27 18:10:54 +00:00
committed by GitHub
parent 354940a8c6
commit 20adb0f16a
3 changed files with 87 additions and 5 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
REDIS_CONNECTION_TYPE: multiplexed
REDIS_URL: redis://127.0.0.1:7000,redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003,redis://127.0.0.1:7004,redis://127.0.0.1:7005
# Avoid stack overflows in tests
RUST_MIN_STACK: 67108864
RUST_MIN_STACK: 134217728
steps:
- name: Check out code
+18 -1
View File
@@ -2,7 +2,8 @@ use std::time::Instant;
use eyre::{WrapErr, eyre};
use neverbounce::{
ResponseStatus, SingleCheckParams, SingleCheckResponse, VerificationResult,
ReqwestErrorReason, ResponseStatus, SingleCheckParams, SingleCheckResponse,
VerificationResult,
};
use tracing::{debug, error};
@@ -27,11 +28,21 @@ pub async fn check_email(email: &str) -> eyre::Result<VerificationResult> {
{
Ok(response) => response,
Err(source) => {
let reason = ReqwestErrorReason::from(&source);
let is_transient = reason.is_transient();
error!(
result = "unknown",
request.error_reason = ?reason,
request.time_ms = check_time_start.elapsed().as_millis(),
error = ?source,
"NeverBounce email check failed",
);
if is_transient {
return Ok(VerificationResult::Unknown);
}
return Err(eyre!(source)).wrap_err("failed to check email");
}
};
@@ -65,16 +76,22 @@ pub async fn check_email(email: &str) -> eyre::Result<VerificationResult> {
}
failure_type => {
let result = result.unwrap_or(VerificationResult::Unknown);
let is_transient = failure_type.is_transient();
error!(
failure_type = response_failure_type(&failure_type),
result = result.as_str(),
request.time_ms = check_time.as_millis(),
"NeverBounce email check failed",
);
if is_transient {
Ok(VerificationResult::Unknown)
} else {
Err(email_check_error_generic())
}
}
}
}
pub fn email_check_error_generic() -> eyre::Error {
eyre!("Please try a different email address!")
+67 -2
View File
@@ -4,7 +4,65 @@ use std::time::Duration;
pub const DEFAULT_API_URL: &str = "https://api.neverbounce.com";
pub const SINGLE_CHECK_PATH: &str = "/v4/single/check";
pub const TIMEOUT: Duration = Duration::from_secs(10);
pub const TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReqwestErrorReason {
Builder,
Redirect,
Status(reqwest::StatusCode),
Timeout,
Request,
Connect,
Body,
Decode,
Unknown,
}
impl ReqwestErrorReason {
#[must_use]
pub fn is_transient(&self) -> bool {
match self {
Self::Status(status) => {
status.is_server_error()
|| matches!(
*status,
reqwest::StatusCode::REQUEST_TIMEOUT
| reqwest::StatusCode::TOO_MANY_REQUESTS
)
}
Self::Timeout | Self::Request | Self::Connect | Self::Body => true,
Self::Builder | Self::Redirect | Self::Decode | Self::Unknown => {
false
}
}
}
}
impl From<&reqwest::Error> for ReqwestErrorReason {
fn from(error: &reqwest::Error) -> Self {
if error.is_timeout() {
Self::Timeout
} else if error.is_connect() {
Self::Connect
} else if error.is_builder() {
Self::Builder
} else if error.is_redirect() {
Self::Redirect
} else if error.is_status() {
error.status().map_or(Self::Unknown, Self::Status)
} else if error.is_request() {
Self::Request
} else if error.is_body() {
Self::Body
} else if error.is_decode() {
Self::Decode
} else {
Self::Unknown
}
}
}
/// Authentication and email parameters for a single verification.
///
@@ -71,6 +129,13 @@ pub enum ResponseStatus {
Unrecognized(String),
}
impl ResponseStatus {
#[must_use]
pub fn is_transient(&self) -> bool {
matches!(self, Self::TemporarilyUnavailable | Self::ThrottleTriggered)
}
}
impl<'de> Deserialize<'de> for ResponseStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@@ -201,7 +266,7 @@ struct SingleCheckRequest<'a> {
/// This endpoint should only be called in response to an action such as a form
/// submission. Existing lists and databases must use NeverBounce's bulk API.
/// Both the server verification timeout and the complete HTTP request timeout
/// are ten seconds.
/// are five seconds.
pub async fn single_check(
client: &Client,
params: &SingleCheckParams<'_>,