parse_rust_server/routes/
batch.rs1use parse_rust_core::{ErrorCode, ErrorOrigin, ParseError};
17use serde_json::{json, Value as Json};
18
19use crate::auth::Authority;
20use crate::params::Params;
21use crate::request::RequestContext;
22use crate::routes::dispatch::{self, RouteError};
23use crate::state::AppState;
24
25const BATCH_PATH: &str = "/batch";
27
28pub async fn handle(
35 state: &AppState,
36 rc: &RequestContext,
37 authority: &Authority,
38 mount_path: &str,
39 body: Option<&Json>,
40) -> Result<Json, ParseError> {
41 let Some(Json::Object(body)) = body else {
42 return Err(ParseError::invalid_json("requests must be an array"));
43 };
44
45 if matches!(body.get("transaction"), Some(Json::Bool(true))) {
46 return Err(ParseError::new(
47 ErrorCode::CommandUnavailable,
48 "Batch transactions are not supported yet. Retry without `transaction: true`; \
49 the sub-requests will be applied independently and reported per operation.",
50 ));
51 }
52
53 let Some(Json::Array(requests)) = body.get("requests") else {
54 return Err(ParseError::invalid_json("requests must be an array"));
55 };
56
57 let limit = state.config().batch_request_limit;
60 if limit > -1 && !authority.is_privileged() && requests.len() as i64 > limit {
61 return Err(ParseError::invalid_json(format!(
62 "Batch request contains {} sub-requests, which exceeds the limit of {limit}.",
63 requests.len()
64 )));
65 }
66
67 let mut parsed = Vec::with_capacity(requests.len());
70 for request in requests {
71 let Json::Object(request) = request else {
72 return Err(ParseError::invalid_json(
73 "batch request path must be a string",
74 ));
75 };
76 let Some(Json::String(path)) = request.get("path") else {
77 return Err(ParseError::invalid_json(
78 "batch request path must be a string",
79 ));
80 };
81 let method = match request.get("method") {
82 Some(Json::String(m)) => m.to_uppercase(),
83 _ => "GET".to_string(),
84 };
85 let routable = routable_path(path, mount_path)?;
86 if method == "POST" && routable == BATCH_PATH {
87 return Err(ParseError::invalid_json(
88 "nested batch requests are not allowed",
89 ));
90 }
91 parsed.push((method, routable, request.get("body").cloned()));
92 }
93
94 let mut results = Vec::with_capacity(parsed.len());
95 for (method, path, body) in parsed {
96 results.push(run_one(state, rc, authority, &method, &path, body.as_ref()).await);
97 }
98 Ok(Json::Array(results))
99}
100
101async fn run_one(
103 state: &AppState,
104 rc: &RequestContext,
105 authority: &Authority,
106 method: &str,
107 path: &str,
108 body: Option<&Json>,
109) -> Json {
110 let Ok(method) = method.parse::<http::Method>() else {
111 return json!({ "error": {
112 "code": ErrorCode::InvalidJson.as_i32(),
113 "error": format!("cannot route {method} {path}"),
114 }});
115 };
116 let Some(route) = dispatch::route_of(path) else {
117 return json!({ "error": {
118 "code": ErrorCode::InvalidJson.as_i32(),
119 "error": format!("cannot route {method} {path}"),
120 }});
121 };
122
123 let params = if matches!(method, http::Method::GET | http::Method::DELETE) {
126 Params::from_json(body)
127 } else {
128 Params::default()
129 };
130
131 let incoming = dispatch::Incoming {
132 method,
133 route,
134 path: path.to_string(),
135 params,
136 body: body.cloned(),
137 };
138 match dispatch::dispatch(state, rc, authority, &incoming).await {
139 Ok(response) => json!({ "success": response.body }),
140 Err(RouteError::Parse(e)) if e.origin == ErrorOrigin::Internal => {
148 json!({ "error": { "error": crate::response::INTERNAL_SERVER_ERROR_MESSAGE }})
149 }
150 Err(RouteError::Parse(e)) => json!({ "error": {
151 "code": e.code.as_i32(),
152 "error": e.message,
153 }}),
154 Err(RouteError::Http(e)) => json!({ "error": { "error": e.message }}),
160 Err(RouteError::NotFound { method, path }) => json!({ "error": {
163 "code": ErrorCode::InvalidJson.as_i32(),
164 "error": format!("cannot route {method} {path}"),
165 }}),
166 }
167}
168
169fn routable_path(path: &str, mount_path: &str) -> Result<String, ParseError> {
174 let prefix = mount_path.trim_end_matches('/');
175 let rest = if prefix.is_empty() {
176 Some(path)
177 } else {
178 path.strip_prefix(prefix)
179 };
180 let Some(rest) = rest else {
181 return Err(ParseError::invalid_json(format!(
182 "cannot route batch path {path}"
183 )));
184 };
185 let trimmed = rest.trim_matches('/');
188 if trimmed.is_empty() {
189 return Ok("/".to_string());
190 }
191 Ok(format!("/{trimmed}"))
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn the_prefix_is_the_configured_mount_and_nothing_else() {
200 assert_eq!(
201 routable_path("/parse/classes/Post", "/parse").expect("routes"),
202 "/classes/Post"
203 );
204 assert_eq!(routable_path("/parse", "/parse").expect("routes"), "/");
205 assert_eq!(
206 routable_path("/classes/Post", "/").expect("routes"),
207 "/classes/Post"
208 );
209 }
210
211 #[test]
212 fn a_path_outside_the_prefix_is_refused_by_name() {
213 let e = routable_path("/other/classes/Post", "/parse").unwrap_err();
214 assert_eq!(e.code, ErrorCode::InvalidJson);
215 assert_eq!(e.message, "cannot route batch path /other/classes/Post");
216 }
217
218 #[test]
221 fn a_prefix_that_only_looks_like_the_mount_still_fails_to_route() {
222 let routable = routable_path("/parsexyz/classes/Post", "/parse").expect("prefix matches");
223 assert_eq!(routable, "/xyz/classes/Post");
224 assert!(dispatch::route_of(&routable).is_none());
225 }
226}