From 8f1146a75e609834681c0706c4ddfee890c170e1 Mon Sep 17 00:00:00 2001 From: Trezy Date: Mon, 09 Mar 2026 16:22:59 +0000 Subject: [PATCH] fix: proper parsing for array query params --- src/lua/context.rs | 6 +++--- src/lua/execute.rs | 2 +- src/xrpc/mod.rs | 47 +++++++++++++++++++++++++++++++++++++++-------- src/xrpc/query.rs | 8 +++++--- 4 file(s) changed, 48 insertion(s)(+), 15 deletion(s)(-) diff --git a/src/lua/context.rs b/src/lua/context.rs --- a/src/lua/context.rs +++ b/src/lua/context.rs @@ -22,7 +22,7 @@ pub fn set_query_context( lua: &Lua, method: &str, - params: &HashMap, + params: &HashMap, collection: &str, ) -> LuaResult<()> { let globals = lua.globals(); @@ -129,8 +129,8 @@ fn query_context_sets_all_globals() { let lua = create_sandbox().unwrap(); let mut params = HashMap::new(); - params.insert("limit".to_string(), "10".to_string()); - params.insert("cursor".to_string(), "abc".to_string()); + params.insert("limit".to_string(), json!("10")); + params.insert("cursor".to_string(), json!("abc")); set_query_context(&lua, "com.example.listThings", ¶ms, "com.example.thing").unwrap(); let globals = lua.globals(); diff --git a/src/lua/execute.rs b/src/lua/execute.rs --- a/src/lua/execute.rs +++ b/src/lua/execute.rs @@ -338,7 +338,7 @@ pub async fn execute_query_script( state: &AppState, method: &str, - params: &HashMap, + params: &HashMap, lexicon: &ParsedLexicon, script: &str, ) -> Result { diff --git a/src/xrpc/mod.rs b/src/xrpc/mod.rs --- a/src/xrpc/mod.rs +++ b/src/xrpc/mod.rs @@ -3,9 +3,10 @@ use axum::Json; use axum::body::Body; -use axum::extract::{Path, Query, State}; +use axum::extract::{Path, RawQuery, State}; use axum::http::StatusCode; use axum::response::Response; +use serde_json::Value; use std::collections::HashMap; use crate::AppState; @@ -13,6 +14,38 @@ use crate::error::AppError; use crate::lexicon::LexiconType; use crate::resolve::resolve_nsid_authority; + +/// Parse a raw query string into a map where repeated keys become JSON arrays. +/// Single-value keys remain as JSON strings for backward compatibility. +fn parse_query_params(query: &str) -> HashMap { + let mut multi: HashMap> = HashMap::new(); + for pair in query.split('&') { + if pair.is_empty() { + continue; + } + let (key, value) = match pair.split_once('=') { + Some((k, v)) => ( + urlencoding::decode(k).unwrap_or_default().into_owned(), + urlencoding::decode(v).unwrap_or_default().into_owned(), + ), + None => ( + urlencoding::decode(pair).unwrap_or_default().into_owned(), + String::new(), + ), + }; + multi.entry(key).or_default().push(value); + } + multi + .into_iter() + .map(|(k, v)| { + if v.len() == 1 { + (k, Value::String(v.into_iter().next().unwrap())) + } else { + (k, Value::Array(v.into_iter().map(Value::String).collect())) + } + }) + .collect() +} /// Proxy an unrecognized XRPC method to its home AppView resolved via DNS. async fn proxy_to_authority( @@ -75,17 +108,15 @@ pub async fn xrpc_get( State(state): State, Path(method): Path, - Query(params): Query>, + RawQuery(raw_query): RawQuery, ) -> Result { + let raw_query = raw_query.unwrap_or_default(); + let params = parse_query_params(&raw_query); + let lexicon = match state.lexicons.get(&method).await { Some(l) => l, None => { - let query_string: String = params - .iter() - .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v))) - .collect::>() - .join("&"); - return proxy_to_authority(&state, &method, &query_string, None).await; + return proxy_to_authority(&state, &method, &raw_query, None).await; } }; diff --git a/src/xrpc/query.rs b/src/xrpc/query.rs --- a/src/xrpc/query.rs +++ b/src/xrpc/query.rs @@ -9,7 +9,7 @@ pub(super) async fn handle_query( state: &AppState, method: &str, - params: &HashMap, + params: &HashMap, lexicon: &crate::lexicon::ParsedLexicon, ) -> Result { if let Some(ref script) = lexicon.script { @@ -17,7 +17,7 @@ } // Single-record query: has a `uri` parameter - if let Some(uri) = params.get("uri") { + if let Some(uri) = params.get("uri").and_then(|v| v.as_str()) { return handle_get_record(state, uri).await; } @@ -30,16 +30,18 @@ let limit: i64 = params .get("limit") + .and_then(|v| v.as_str()) .and_then(|l| l.parse().ok()) .unwrap_or(20) .min(100); let offset: i64 = params .get("cursor") + .and_then(|v| v.as_str()) .and_then(|c| c.parse().ok()) .unwrap_or(0); - let did = params.get("did"); + let did = params.get("did").and_then(|v| v.as_str()); let rows: Vec<(String, String, Value)> = if let Some(did) = did { sqlx::query_as( -- tangled.sh