fix(executor): add Content-Length: 0 for body-less POST requests (#183)

* fix(executor): add Content-Length: 0 header for body-less POST/PUT/PATCH requests

Google API servers return HTTP 411 (Length Required) when a POST request
is sent without a Content-Length header, even if there is no body.
This affects all Discovery API methods where httpMethod is POST but no
requestBody is defined (e.g. gmail users.messages.trash).

Fixes #182

* chore: add changeset for content-length fix
This commit is contained in:
Frank
2026-03-06 05:10:02 +08:00
committed by GitHub
parent 88cb65ce80
commit cb1f988344
2 changed files with 88 additions and 0 deletions
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Add Content-Length: 0 header for POST/PUT/PATCH requests with no body to fix HTTP 411 errors
+83
View File
@@ -191,6 +191,8 @@ async fn build_http_request(
} else if let Some(ref body_val) = input.body {
request = request.header("Content-Type", "application/json");
request = request.json(body_val);
} else if matches!(method.http_method.as_str(), "POST" | "PUT" | "PATCH") {
request = request.header("Content-Length", "0");
}
}
@@ -1817,3 +1819,84 @@ fn test_get_value_type_helper() {
assert_eq!(get_value_type(&json!([1, 2])), "array");
assert_eq!(get_value_type(&json!({"a": 1})), "object");
}
#[tokio::test]
async fn test_post_without_body_sets_content_length_zero() {
let client = reqwest::Client::new();
let method = RestMethod {
http_method: "POST".to_string(),
path: "messages/trash".to_string(),
..Default::default()
};
let input = ExecutionInput {
full_url: "https://example.com/messages/trash".to_string(),
body: None,
params: Map::new(),
query_params: HashMap::new(),
is_upload: false,
};
let request = build_http_request(&client, &method, &input, None, &AuthMethod::None, None, 0, None)
.await
.unwrap();
let built = request.build().unwrap();
assert_eq!(
built.headers().get("Content-Length").map(|v| v.to_str().unwrap()),
Some("0"),
"POST with no body must include Content-Length: 0"
);
}
#[tokio::test]
async fn test_post_with_body_does_not_add_content_length_zero() {
let client = reqwest::Client::new();
let method = RestMethod {
http_method: "POST".to_string(),
path: "files".to_string(),
..Default::default()
};
let input = ExecutionInput {
full_url: "https://example.com/files".to_string(),
body: Some(json!({"name": "test"})),
params: Map::new(),
query_params: HashMap::new(),
is_upload: false,
};
let request = build_http_request(&client, &method, &input, None, &AuthMethod::None, None, 0, None)
.await
.unwrap();
let built = request.build().unwrap();
// When body is present, Content-Length should NOT be "0"
let cl = built.headers().get("Content-Length").map(|v| v.to_str().unwrap().to_string());
assert!(cl.is_none() || cl.as_deref() != Some("0"));
}
#[tokio::test]
async fn test_get_does_not_set_content_length_zero() {
let client = reqwest::Client::new();
let method = RestMethod {
http_method: "GET".to_string(),
path: "files".to_string(),
..Default::default()
};
let input = ExecutionInput {
full_url: "https://example.com/files".to_string(),
body: None,
params: Map::new(),
query_params: HashMap::new(),
is_upload: false,
};
let request = build_http_request(&client, &method, &input, None, &AuthMethod::None, None, 0, None)
.await
.unwrap();
let built = request.build().unwrap();
assert!(
built.headers().get("Content-Length").is_none(),
"GET with no body should not have Content-Length header"
);
}