Troubleshooting search functionality issues in Sanity.io
I've seen this issue before! A 400 error with "invalid character" in Sanity search is usually caused by special characters in your search input that aren't properly handled when building GROQ queries. Let me walk you through what's happening and how to fix it.
The Root Cause
When you're building search queries dynamically (especially with the match operator), characters like quotes (" or '), backslashes (\), or other special characters can break your GROQ query syntax if you're using string interpolation:
// ❌ This breaks if searchTerm contains quotes or special chars
const query = `*[_type == "post" && title match "${searchTerm}"]`
// User types: demo"test
// Results in: *[_type == "post" && title match "demo"test"]
// → 400 error: invalid characterThe Solution: Use GROQ Parameters
The best and safest approach is to use GROQ parameters instead of string interpolation. Parameters are automatically escaped by the Sanity client, preventing syntax errors:
// ✅ Safe approach using parameters
const query = `*[_type == "post" && title match $searchTerm]`
const params = { searchTerm: `*${userInput}*` }
const results = await client.fetch(query, params)With parameters, the Sanity client handles all the escaping for you, so special characters won't break your query.
Understanding the match Operator
One important thing to know: the match operator works on tokenized text, not regex patterns. It breaks text into searchable words based on word boundaries and special characters. For example, "user@sanity.io" is tokenized as ['user', 'sanity', 'io'].
This means:
- You don't need to escape regex characters for
matchto work correctly - Special characters like
@,.,/act as word separators - Wildcards (
*) are supported for prefix matching
Quick Fix for Custom Studio Search
Here's a robust implementation:
import { client } from './sanityClient'
async function search(userInput) {
const sanitized = userInput.trim()
if (!sanitized) return []
// Using parameters (recommended)
const query = `*[_type == "post" && title match $searchTerm] | order(_createdAt desc)`
const params = { searchTerm: `*${sanitized}*` }
try {
return await client.fetch(query, params)
} catch (error) {
console.error('Search error:', error)
return []
}
}Debugging Steps
- Check the Network tab - Look at the exact GROQ query being sent in your browser's developer tools
- Test with simple input - Try searching with just letters first, then gradually add special characters to identify which one causes the issue
- Check your query construction - If you're using template literals with
${}, switch to GROQ parameters with$variableName
If You're Using the HTTP API Directly
If you're calling the HTTP API directly (not through the JavaScript client), make sure your query is properly URL-encoded:
const encodedQuery = encodeURIComponent(query)The JavaScript client handles this automatically, which is another reason to use parameters!
Let me know if you're still seeing the 400 error after switching to parameters - it would help to see the exact query that's failing in your Network tab. The error message should also show which character is causing the issue.
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.