semantic bufo search find-bufo.com
bufo
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve error handling for overly long search queries

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 <noreply@anthropic.com>

+61 -12
+1
Cargo.lock
··· 686 686 "reqwest", 687 687 "serde", 688 688 "serde_json", 689 + "thiserror 1.0.69", 689 690 "tokio", 690 691 "tracing", 691 692 ]
+1
Cargo.toml
··· 12 12 tokio = { version = "1", features = ["full"] } 13 13 reqwest = { version = "0.12", features = ["json", "multipart"] } 14 14 anyhow = "1.0" 15 + thiserror = "1.0" 15 16 dotenv = "0.15" 16 17 base64 = "0.22" 17 18 actix-governor = "0.10.0"
+16 -5
src/search.rs
··· 42 42 43 43 use crate::config::Config; 44 44 use crate::embedding::EmbeddingClient; 45 - use crate::turbopuffer::{QueryRequest, TurbopufferClient}; 45 + use crate::turbopuffer::{QueryRequest, TurbopufferClient, TurbopufferError}; 46 46 use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult}; 47 47 use serde::{Deserialize, Serialize}; 48 48 use std::collections::hash_map::DefaultHasher; ··· 228 228 query = &query_text, 229 229 top_k = search_top_k as i64 230 230 ); 231 - actix_web::error::ErrorInternalServerError(format!( 232 - "failed to query turbopuffer (BM25): {}", 233 - e 234 - )) 231 + 232 + // return appropriate HTTP status based on error type 233 + match e { 234 + TurbopufferError::QueryTooLong { .. } => { 235 + actix_web::error::ErrorBadRequest( 236 + "search query is too long (max 1024 characters for text search). try a shorter query." 237 + ) 238 + } 239 + _ => { 240 + actix_web::error::ErrorInternalServerError(format!( 241 + "failed to query turbopuffer (BM25): {}", 242 + e 243 + )) 244 + } 245 + } 235 246 })? 236 247 }; 237 248
+43 -7
src/turbopuffer.rs
··· 1 1 use anyhow::{Context, Result}; 2 2 use reqwest::Client; 3 3 use serde::{Deserialize, Serialize}; 4 + use thiserror::Error; 5 + 6 + #[derive(Debug, Error)] 7 + pub enum TurbopufferError { 8 + #[error("query too long: {message}")] 9 + QueryTooLong { message: String }, 10 + #[error("turbopuffer API error: {0}")] 11 + ApiError(String), 12 + #[error("request failed: {0}")] 13 + RequestFailed(#[from] reqwest::Error), 14 + #[error("{0}")] 15 + Other(#[from] anyhow::Error), 16 + } 17 + 18 + #[derive(Debug, Deserialize)] 19 + struct TurbopufferErrorResponse { 20 + error: String, 21 + #[allow(dead_code)] 22 + status: String, 23 + } 4 24 5 25 #[derive(Debug, Serialize)] 6 26 pub struct QueryRequest { ··· 64 84 .context(format!("failed to parse query response: {}", body)) 65 85 } 66 86 67 - pub async fn bm25_query(&self, query_text: &str, top_k: usize) -> Result<QueryResponse> { 87 + pub async fn bm25_query(&self, query_text: &str, top_k: usize) -> Result<QueryResponse, TurbopufferError> { 68 88 let url = format!( 69 89 "https://api.turbopuffer.com/v1/vectors/{}/query", 70 90 self.namespace ··· 76 96 "include_attributes": ["url", "name", "filename"], 77 97 }); 78 98 79 - log::debug!("turbopuffer BM25 query request: {}", serde_json::to_string_pretty(&request)?); 99 + if let Ok(pretty) = serde_json::to_string_pretty(&request) { 100 + log::debug!("turbopuffer BM25 query request: {}", pretty); 101 + } 80 102 81 103 let response = self 82 104 .client ··· 84 106 .header("Authorization", format!("Bearer {}", self.api_key)) 85 107 .json(&request) 86 108 .send() 87 - .await 88 - .context("failed to send BM25 query request")?; 109 + .await?; 89 110 90 111 if !response.status().is_success() { 91 112 let status = response.status(); 92 113 let body = response.text().await.unwrap_or_default(); 93 - anyhow::bail!("turbopuffer BM25 query failed with status {}: {}", status, body); 114 + 115 + // try to parse turbopuffer error response 116 + if let Ok(error_resp) = serde_json::from_str::<TurbopufferErrorResponse>(&body) { 117 + // check if it's a query length error 118 + if error_resp.error.contains("too long") && error_resp.error.contains("max 1024") { 119 + return Err(TurbopufferError::QueryTooLong { 120 + message: error_resp.error, 121 + }); 122 + } 123 + } 124 + 125 + return Err(TurbopufferError::ApiError(format!( 126 + "turbopuffer BM25 query failed with status {}: {}", 127 + status, body 128 + ))); 94 129 } 95 130 96 - let body = response.text().await.context("failed to read response body")?; 131 + let body = response.text().await 132 + .map_err(|e| TurbopufferError::Other(anyhow::anyhow!("failed to read response body: {}", e)))?; 97 133 log::debug!("turbopuffer BM25 response: {}", body); 98 134 99 135 let parsed: QueryResponse = serde_json::from_str(&body) 100 - .context(format!("failed to parse BM25 query response: {}", body))?; 136 + .map_err(|e| TurbopufferError::Other(anyhow::anyhow!("failed to parse BM25 query response: {}", e)))?; 101 137 102 138 // DEBUG: log first result to see what BM25 returns 103 139 if let Some(first) = parsed.first() {