From 02b41133f8fa5adcf250a44f9bebac96225d6164 Mon Sep 17 00:00:00 2001 From: Ken Loeffler Date: Mon, 19 Jan 2026 14:44:42 -0800 Subject: [PATCH] Use post for ref patch and serialize (#1192) --- CHANGELOG.md | 2 ++ plugin/src/ApiContext.lua | 40 ++++++++++++--------- src/web/api.rs | 65 +++++++++++++++++++---------------- src/web/interface.rs | 14 ++++++++ tests/rojo_test/serve_util.rs | 30 +++++++++------- tests/tests/serve.rs | 6 ++-- 6 files changed, 98 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7843cb44..7e6aacdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,10 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased * Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179]) +* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192]) [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 +[#1192]: https://github.com/rojo-rbx/rojo/pull/1192 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/plugin/src/ApiContext.lua b/plugin/src/ApiContext.lua index cbbffcc1..2d2459ec 100644 --- a/plugin/src/ApiContext.lua +++ b/plugin/src/ApiContext.lua @@ -290,31 +290,39 @@ function ApiContext:open(id) end function ApiContext:serialize(ids: { string }) - local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ",")) + local url = ("%s/api/serialize"):format(self.__baseUrl) + local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids }) - return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) - if body.sessionId ~= self.__sessionId then - return Promise.reject("Server changed ID") - end + return Http.post(url, request_body) + :andThen(rejectFailedRequests) + :andThen(Http.Response.json) + :andThen(function(response_body) + if response_body.sessionId ~= self.__sessionId then + return Promise.reject("Server changed ID") + end - assert(validateApiSerialize(body)) + assert(validateApiSerialize(response_body)) - return body - end) + return response_body + end) end function ApiContext:refPatch(ids: { string }) - local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ",")) + local url = ("%s/api/ref-patch"):format(self.__baseUrl) + local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids }) - return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body) - if body.sessionId ~= self.__sessionId then - return Promise.reject("Server changed ID") - end + return Http.post(url, request_body) + :andThen(rejectFailedRequests) + :andThen(Http.Response.json) + :andThen(function(response_body) + if response_body.sessionId ~= self.__sessionId then + return Promise.reject("Server changed ID") + end - assert(validateApiRefPatch(body)) + assert(validateApiRefPatch(response_body)) - return body - end) + return response_body + end) end return ApiContext diff --git a/src/web/api.rs b/src/web/api.rs index 8e7ce72f..5fbd7b6d 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -1,13 +1,7 @@ //! Defines Rojo's HTTP API, all under /api. These endpoints generally return //! JSON. -use std::{ - collections::{HashMap, HashSet}, - fs, - path::PathBuf, - str::FromStr, - sync::Arc, -}; +use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc}; use futures::{sink::SinkExt, stream::StreamExt}; use hyper::{body, Body, Method, Request, Response, StatusCode}; @@ -30,7 +24,10 @@ use crate::{ }, util::{json, json_ok}, }, - web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse}, + web_api::{ + BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest, + SerializeResponse, + }, }; pub async fn call(serve_session: Arc, mut request: Request) -> Response { @@ -53,12 +50,8 @@ pub async fn call(serve_session: Arc, mut request: Request) ) } } - (&Method::GET, path) if path.starts_with("/api/serialize/") => { - service.handle_api_serialize(request).await - } - (&Method::GET, path) if path.starts_with("/api/ref-patch/") => { - service.handle_api_ref_patch(request).await - } + (&Method::POST, "/api/serialize") => service.handle_api_serialize(request).await, + (&Method::POST, "/api/ref-patch") => service.handle_api_ref_patch(request).await, (&Method::POST, path) if path.starts_with("/api/open/") => { service.handle_api_open(request).await @@ -229,22 +222,30 @@ impl ApiService { /// that correspond to the requested Instances. These values have their /// `Value` property set to point to the requested Instance. async fn handle_api_serialize(&self, request: Request) -> Response { - let argument = &request.uri().path()["/api/serialize/".len()..]; - let requested_ids: Result, _> = argument.split(',').map(Ref::from_str).collect(); + let session_id = self.serve_session.session_id(); + let body = body::to_bytes(request.into_body()).await.unwrap(); - let requested_ids = match requested_ids { - Ok(ids) => ids, - Err(_) => { + let request: SerializeRequest = match json::from_slice(&body) { + Ok(request) => request, + Err(err) => { return json( - ErrorResponse::bad_request("Malformed ID list"), + ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); } }; + + if request.session_id != session_id { + return json( + ErrorResponse::bad_request("Wrong session ID"), + StatusCode::BAD_REQUEST, + ); + } + let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder")); let tree = self.serve_session.tree(); - for id in &requested_ids { + for id in &request.ids { if let Some(instance) = tree.get_instance(*id) { let clone = response_dom.insert( Ref::none(), @@ -290,20 +291,26 @@ impl ApiService { /// and referent properties need to be updated after the serialize /// endpoint is used. async fn handle_api_ref_patch(self, request: Request) -> Response { - let argument = &request.uri().path()["/api/ref-patch/".len()..]; - let requested_ids: Result, _> = - argument.split(',').map(Ref::from_str).collect(); + let session_id = self.serve_session.session_id(); + let body = body::to_bytes(request.into_body()).await.unwrap(); - let requested_ids = match requested_ids { - Ok(ids) => ids, - Err(_) => { + let request: RefPatchRequest = match json::from_slice(&body) { + Ok(request) => request, + Err(err) => { return json( - ErrorResponse::bad_request("Malformed ID list"), + ErrorResponse::bad_request(format!("Invalid body: {}", err)), StatusCode::BAD_REQUEST, ); } }; + if request.session_id != session_id { + return json( + ErrorResponse::bad_request("Wrong session ID"), + StatusCode::BAD_REQUEST, + ); + } + let mut instance_updates: HashMap = HashMap::new(); let tree = self.serve_session.tree(); @@ -312,7 +319,7 @@ impl ApiService { let Variant::Ref(prop_value) = prop_value else { continue; }; - if let Some(target_id) = requested_ids.get(prop_value) { + if let Some(target_id) = request.ids.get(prop_value) { let instance_id = instance.id(); let update = instance_updates diff --git a/src/web/interface.rs b/src/web/interface.rs index 4871ec97..430f13ec 100644 --- a/src/web/interface.rs +++ b/src/web/interface.rs @@ -238,6 +238,13 @@ pub struct OpenResponse { pub session_id: SessionId, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SerializeRequest { + pub session_id: SessionId, + pub ids: Vec, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SerializeResponse { @@ -269,6 +276,13 @@ impl BufferEncode { } } +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefPatchRequest { + pub session_id: SessionId, + pub ids: HashSet, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RefPatchResponse<'a> { diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index 9036316f..a5decdbb 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -1,5 +1,4 @@ use std::{ - fmt::Write as _, fs, path::{Path, PathBuf}, process::Command, @@ -13,8 +12,12 @@ use rbx_dom_weak::types::Ref; use tempfile::{tempdir, TempDir}; -use librojo::web_api::{ - ReadResponse, SerializeResponse, ServerInfoResponse, SocketPacket, SocketPacketType, +use librojo::{ + web_api::{ + ReadResponse, SerializeRequest, SerializeResponse, ServerInfoResponse, SocketPacket, + SocketPacketType, + }, + SessionId, }; use rojo_insta_ext::RedactionMap; @@ -226,16 +229,19 @@ impl TestServeSession { } } - pub fn get_api_serialize(&self, ids: &[Ref]) -> Result { - let mut id_list = String::with_capacity(ids.len() * 33); - for id in ids { - write!(id_list, "{id},").unwrap(); - } - id_list.pop(); + pub fn get_api_serialize( + &self, + ids: &[Ref], + session_id: SessionId, + ) -> Result { + let client = reqwest::blocking::Client::new(); + let url = format!("http://localhost:{}/api/serialize", self.port); + let body = serde_json::to_string(&SerializeRequest { + session_id, + ids: ids.to_vec(), + }); - let url = format!("http://localhost:{}/api/serialize/{}", self.port, id_list); - - reqwest::blocking::get(url)?.json() + client.post(url).body((body).unwrap()).send()?.json() } } diff --git a/tests/tests/serve.rs b/tests/tests/serve.rs index ee0528e1..748f1687 100644 --- a/tests/tests/serve.rs +++ b/tests/tests/serve.rs @@ -646,7 +646,7 @@ fn meshpart_with_id() { .unwrap(); let serialize_response = session - .get_api_serialize(&[*meshpart, *objectvalue]) + .get_api_serialize(&[*meshpart, *objectvalue], info.session_id) .unwrap(); // We don't assert a snapshot on the SerializeResponse because the model includes the @@ -673,7 +673,9 @@ fn forced_parent() { read_response.intern_and_redact(&mut redactions, root_id) ); - let serialize_response = session.get_api_serialize(&[root_id]).unwrap(); + let serialize_response = session + .get_api_serialize(&[root_id], info.session_id) + .unwrap(); assert_eq!(serialize_response.session_id, info.session_id);