From 20adb0f16a43285d320206de64bea6cb76c6b925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Talbot?= <108630700+fetchfern@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:10:54 -0400 Subject: [PATCH 1/6] 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 --- .github/workflows/turbo-ci.yml | 2 +- apps/labrinth/src/util/neverbounce.rs | 21 +++++++- packages/neverbounce/src/lib.rs | 69 ++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/.github/workflows/turbo-ci.yml b/.github/workflows/turbo-ci.yml index 4088a97322..768b7a64b5 100644 --- a/.github/workflows/turbo-ci.yml +++ b/.github/workflows/turbo-ci.yml @@ -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 diff --git a/apps/labrinth/src/util/neverbounce.rs b/apps/labrinth/src/util/neverbounce.rs index f104deb649..ffab561d8d 100644 --- a/apps/labrinth/src/util/neverbounce.rs +++ b/apps/labrinth/src/util/neverbounce.rs @@ -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 { { 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,13 +76,19 @@ pub async fn check_email(email: &str) -> eyre::Result { } 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", ); - Err(email_check_error_generic()) + + if is_transient { + Ok(VerificationResult::Unknown) + } else { + Err(email_check_error_generic()) + } } } } diff --git a/packages/neverbounce/src/lib.rs b/packages/neverbounce/src/lib.rs index 74c6af5b1b..b4390c0a15 100644 --- a/packages/neverbounce/src/lib.rs +++ b/packages/neverbounce/src/lib.rs @@ -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(deserializer: D) -> Result 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<'_>, From c938ca3ceb2f40bc2327b6937ef87282ed1772c2 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:49:52 -0700 Subject: [PATCH 2/6] update toggle filter design (#6888) change toggle filters to just use exclude-only filters, same design as the others. --- .../components/search/SearchFilterGroup.vue | 6 +- .../components/search/SearchFilterOption.vue | 28 ++- .../components/search/SearchSidebarFilter.vue | 174 ++++++++---------- packages/ui/src/utils/search.ts | 29 +-- packages/ui/src/utils/server-search.ts | 12 +- 5 files changed, 118 insertions(+), 131 deletions(-) diff --git a/packages/ui/src/components/search/SearchFilterGroup.vue b/packages/ui/src/components/search/SearchFilterGroup.vue index 071d0896c1..4a23f1742b 100644 --- a/packages/ui/src/components/search/SearchFilterGroup.vue +++ b/packages/ui/src/components/search/SearchFilterGroup.vue @@ -19,7 +19,7 @@ :option="option" :included="included(option)" :excluded="excluded(option)" - :supports-negative-filter="supportsNegativeFilter" + :supports="supports" @toggle="(o) => emit('toggle', o)" @toggle-exclude="(o) => emit('toggleExclude', o)" > @@ -44,13 +44,13 @@ import { DropdownIcon } from '@modrinth/assets' import { ref } from 'vue' -import type { FilterOption } from '../../utils/search' +import type { FilterMode, FilterOption } from '../../utils/search' import SearchFilterOption from './SearchFilterOption.vue' defineProps<{ groupName: string options: FilterOption[] - supportsNegativeFilter: boolean + supports: FilterMode[] included: (option: FilterOption) => boolean excluded: (option: FilterOption) => boolean }>() diff --git a/packages/ui/src/components/search/SearchFilterOption.vue b/packages/ui/src/components/search/SearchFilterOption.vue index f455a273d0..b05b50fc45 100644 --- a/packages/ui/src/components/search/SearchFilterOption.vue +++ b/packages/ui/src/components/search/SearchFilterOption.vue @@ -1,13 +1,13 @@