diff --git a/Cargo.lock b/Cargo.lock index 125e137..aac15ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,6 +1473,7 @@ dependencies = [ "sentry", "serde", "serde_json", + "serde_urlencoded", "serde_variant", "sha2", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index e379dde..2cc5dd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ path = "src/main.rs" [dependencies] serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.149" +serde_urlencoded = "0.7.1" tokio = { version = "1.49.0", features = [ "signal", "rt-multi-thread", diff --git a/src/aliyun/cdn.rs b/src/aliyun/cdn.rs index 43c962d..d1b7aef 100644 --- a/src/aliyun/cdn.rs +++ b/src/aliyun/cdn.rs @@ -119,6 +119,20 @@ pub struct RefreshObjectCachesRequest { pub force: Option, } +/// Form parameters for RefreshObjectCaches API +/// This struct is used for URL encoding the request body +#[derive(Debug, Clone, Serialize)] +struct RefreshObjectCachesFormParams { + #[serde(rename = "ObjectPath")] + object_path: String, + + #[serde(rename = "ObjectType", skip_serializing_if = "Option::is_none")] + object_type: Option, + + #[serde(rename = "Force", skip_serializing_if = "Option::is_none")] + force: Option, +} + /// Response from RefreshObjectCaches API #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RefreshObjectCachesResponse { @@ -257,17 +271,14 @@ impl AliyunCdnClient { ) -> AppResult { // RefreshObjectCaches is a POST request with parameters in an HTML form body. // Reference: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-refreshobjectcaches - let mut form_params = BTreeMap::new(); - form_params.insert("ObjectPath".to_string(), request.object_path.clone()); - - if let Some(ref object_type) = request.object_type { - form_params.insert("ObjectType".to_string(), object_type.clone()); - } - if let Some(force) = request.force { - form_params.insert("Force".to_string(), force.to_string()); - } + let form_params = RefreshObjectCachesFormParams { + object_path: request.object_path.clone(), + object_type: request.object_type.clone(), + force: request.force, + }; - let form_body = build_form_urlencoded_body(&form_params); + let form_body = serde_urlencoded::to_string(&form_params) + .context("Failed to encode form parameters")?; // Sign the request (ACS3-HMAC-SHA256). For this API, the form body must be included // in the body hash, so keep the canonical query empty. @@ -327,29 +338,3 @@ impl AliyunCdnClient { Ok(result) } } - -fn build_form_urlencoded_body(params: &BTreeMap) -> String { - params - .iter() - .map(|(k, v)| format!("{}={}", form_urlencode(k), form_urlencode(v))) - .collect::>() - .join("&") -} - -// application/x-www-form-urlencoded encoding. -// - Space becomes '+' -// - Unreserved characters are not escaped -// - Everything else is percent-encoded with upper-case hex -fn form_urlencode(input: &str) -> String { - let mut out = String::with_capacity(input.len()); - for &b in input.as_bytes() { - match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - out.push(b as char) - } - b' ' => out.push('+'), - _ => out.push_str(&format!("%{:02X}", b)), - } - } - out -}