Skip to content

Commit b3e7ab6

Browse files
fix: handle empty 200 response from PUT /v1/license (#39)
The Redis Enterprise API returns 200 with an empty body for license uploads, but the client tried to deserialize it as JSON, causing an EOF parse error even though the upload succeeded. Adds put_action() method (matching existing post_action pattern) for endpoints that return no content. LicenseHandler::update() now uses put_action + a follow-up GET to return the installed license. Fixes redis-developer/redisctl#895.
1 parent f941c52 commit b3e7ab6

File tree

3 files changed

+43
-1
lines changed

3 files changed

+43
-1
lines changed

src/client.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,34 @@ impl EnterpriseClient {
489489
}
490490
}
491491

492+
/// PUT request for actions that return no content (or may return an empty body)
493+
pub async fn put_action<B: Serialize>(&self, path: &str, body: &B) -> Result<()> {
494+
let url = self.normalize_url(path);
495+
debug!("PUT {}", url);
496+
trace!("Request body: {:?}", serde_json::to_value(body).ok());
497+
498+
let response = self
499+
.client
500+
.put(&url)
501+
.basic_auth(&self.username, Some(&self.password))
502+
.json(body)
503+
.send()
504+
.await
505+
.map_err(|e| self.map_reqwest_error(e, &url))?;
506+
507+
trace!("Response status: {}", response.status());
508+
if response.status().is_success() {
509+
Ok(())
510+
} else {
511+
let status = response.status();
512+
let text = response.text().await.unwrap_or_default();
513+
Err(RestError::ApiError {
514+
code: status.as_u16(),
515+
message: text,
516+
})
517+
}
518+
}
519+
492520
/// POST request with multipart/form-data for file uploads
493521
pub async fn post_multipart<T: DeserializeOwned>(
494522
&self,

src/license.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,13 @@ impl LicenseHandler {
116116
}
117117

118118
/// Update license
119+
///
120+
/// The Redis Enterprise API may return 200 with an empty body for this
121+
/// endpoint, so we use put_action (which tolerates empty responses) and
122+
/// follow up with a GET to return the installed license.
119123
pub async fn update(&self, request: LicenseUpdateRequest) -> Result<License> {
120-
self.client.put("/v1/license", &request).await
124+
self.client.put_action("/v1/license", &request).await?;
125+
self.get().await
121126
}
122127

123128
/// Get license usage statistics

tests/license_tests.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,19 @@ async fn test_license_update() {
104104
license: "new-license-key-12345".to_string(),
105105
};
106106

107+
// PUT /v1/license returns 200 with empty body (real API behavior)
107108
Mock::given(method("PUT"))
108109
.and(path("/v1/license"))
109110
.and(basic_auth("admin", "password"))
110111
.and(body_json(&update_request))
112+
.respond_with(ResponseTemplate::new(200))
113+
.mount(&mock_server)
114+
.await;
115+
116+
// Follow-up GET /v1/license returns the installed license
117+
Mock::given(method("GET"))
118+
.and(path("/v1/license"))
119+
.and(basic_auth("admin", "password"))
111120
.respond_with(success_response(json!({
112121
"key": "new-license-key-12345",
113122
"type": "production",

0 commit comments

Comments
 (0)