From f0d55d7cd795db89367c76e1c568a41e8e51b867 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Thu, 6 Nov 2025 10:33:38 -0600 Subject: [PATCH] improve error handling for overly long search queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit when users submit search queries exceeding turbopuffer's 1024 character limit for BM25 text search, the application now returns a 400 Bad Request with a helpful error message instead of a generic 500 Internal Server Error. changes: - add TurbopufferError enum to categorize different error types - parse turbopuffer API error responses to detect query length violations - return 400 status with user-friendly message for query length errors - maintain 500 status for genuine server errors this fix ensures users understand the limitation and can adjust their queries accordingly, without falsely suggesting a server-side problem. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Cargo.lock | 1 + Cargo.toml | 1 + src/search.rs | 21 ++++++++++++++----- src/turbopuffer.rs | 50 +++++++++++++++++++++++++++++++++++++++------- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f84a37..0a27078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,6 +686,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "thiserror 1.0.69", "tokio", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index ff4492c..15b0999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ serde_json = "1.0" tokio = { version = "1", features = ["full"] } reqwest = { version = "0.12", features = ["json", "multipart"] } anyhow = "1.0" +thiserror = "1.0" dotenv = "0.15" base64 = "0.22" actix-governor = "0.10.0" diff --git a/src/search.rs b/src/search.rs index 0a841af..3e3528a 100644 --- a/src/search.rs +++ b/src/search.rs @@ -42,7 +42,7 @@ use crate::config::Config; use crate::embedding::EmbeddingClient; -use crate::turbopuffer::{QueryRequest, TurbopufferClient}; +use crate::turbopuffer::{QueryRequest, TurbopufferClient, TurbopufferError}; use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult}; use serde::{Deserialize, Serialize}; use std::collections::hash_map::DefaultHasher; @@ -228,10 +228,21 @@ async fn perform_search( query = &query_text, top_k = search_top_k as i64 ); - actix_web::error::ErrorInternalServerError(format!( - "failed to query turbopuffer (BM25): {}", - e - )) + + // return appropriate HTTP status based on error type + match e { + TurbopufferError::QueryTooLong { .. } => { + actix_web::error::ErrorBadRequest( + "search query is too long (max 1024 characters for text search). try a shorter query." + ) + } + _ => { + actix_web::error::ErrorInternalServerError(format!( + "failed to query turbopuffer (BM25): {}", + e + )) + } + } })? }; diff --git a/src/turbopuffer.rs b/src/turbopuffer.rs index a9601c3..6098364 100644 --- a/src/turbopuffer.rs +++ b/src/turbopuffer.rs @@ -1,6 +1,26 @@ use anyhow::{Context, Result}; use reqwest::Client; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum TurbopufferError { + #[error("query too long: {message}")] + QueryTooLong { message: String }, + #[error("turbopuffer API error: {0}")] + ApiError(String), + #[error("request failed: {0}")] + RequestFailed(#[from] reqwest::Error), + #[error("{0}")] + Other(#[from] anyhow::Error), +} + +#[derive(Debug, Deserialize)] +struct TurbopufferErrorResponse { + error: String, + #[allow(dead_code)] + status: String, +} #[derive(Debug, Serialize)] pub struct QueryRequest { @@ -64,7 +84,7 @@ impl TurbopufferClient { .context(format!("failed to parse query response: {}", body)) } - pub async fn bm25_query(&self, query_text: &str, top_k: usize) -> Result { + pub async fn bm25_query(&self, query_text: &str, top_k: usize) -> Result { let url = format!( "https://api.turbopuffer.com/v1/vectors/{}/query", self.namespace @@ -76,7 +96,9 @@ impl TurbopufferClient { "include_attributes": ["url", "name", "filename"], }); - log::debug!("turbopuffer BM25 query request: {}", serde_json::to_string_pretty(&request)?); + if let Ok(pretty) = serde_json::to_string_pretty(&request) { + log::debug!("turbopuffer BM25 query request: {}", pretty); + } let response = self .client @@ -84,20 +106,34 @@ impl TurbopufferClient { .header("Authorization", format!("Bearer {}", self.api_key)) .json(&request) .send() - .await - .context("failed to send BM25 query request")?; + .await?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); - anyhow::bail!("turbopuffer BM25 query failed with status {}: {}", status, body); + + // try to parse turbopuffer error response + if let Ok(error_resp) = serde_json::from_str::(&body) { + // check if it's a query length error + if error_resp.error.contains("too long") && error_resp.error.contains("max 1024") { + return Err(TurbopufferError::QueryTooLong { + message: error_resp.error, + }); + } + } + + return Err(TurbopufferError::ApiError(format!( + "turbopuffer BM25 query failed with status {}: {}", + status, body + ))); } - let body = response.text().await.context("failed to read response body")?; + let body = response.text().await + .map_err(|e| TurbopufferError::Other(anyhow::anyhow!("failed to read response body: {}", e)))?; log::debug!("turbopuffer BM25 response: {}", body); let parsed: QueryResponse = serde_json::from_str(&body) - .context(format!("failed to parse BM25 query response: {}", body))?; + .map_err(|e| TurbopufferError::Other(anyhow::anyhow!("failed to parse BM25 query response: {}", e)))?; // DEBUG: log first result to see what BM25 returns if let Some(first) = parsed.first() { -- 2.43.0 From 57c5d2296f31d1c4595ef4f64fd1989d52265582 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Thu, 6 Nov 2025 10:41:49 -0600 Subject: [PATCH] fix frontend to display backend error messages to users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the backend was correctly returning helpful error messages (like "search query is too long..."), but the frontend was only showing "search failed: Bad Request" because it only read response.statusText and never read the response body. now when an API error occurs, the frontend: 1. reads the response body text 2. displays the actual error message from the backend 3. falls back to statusText if body reading fails this means users will now see the full helpful message: "search query is too long (max 1024 characters for text search). try a shorter query." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- static/index.html | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index aadb3b3..0af4f24 100644 --- a/static/index.html +++ b/static/index.html @@ -691,7 +691,18 @@ }); if (!response.ok) { - throw new Error(`search failed: ${response.statusText}`); + // try to extract error message from response body + let errorMessage = response.statusText; + try { + const errorText = await response.text(); + // actix-web returns plain text error messages, not JSON + if (errorText) { + errorMessage = errorText; + } + } catch (e) { + // if reading body fails, use the status text + } + throw new Error(errorMessage); } const data = await response.json(); -- 2.43.0