fix(client): add 10s connect timeout to prevent initial hangs (#608)
* fix(client): add 10s connect timeout to prevent initial hangs * fix(client): retry on transient connect/timeout errors * chore: regenerate skills [skip ci] --------- Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com> Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@googleworkspace/cli": patch
|
||||
---
|
||||
|
||||
fix(client): add 10s connect timeout to prevent hangs on initial connection
|
||||
@@ -70,19 +70,19 @@ gws drive <resource> <method> [flags]
|
||||
- `create` — Creates a shared drive. For more information, see [Manage shared drives](https://developers.google.com/workspace/drive/api/guides/manage-shareddrives).
|
||||
- `get` — Gets a shared drive's metadata by ID. For more information, see [Manage shared drives](https://developers.google.com/workspace/drive/api/guides/manage-shareddrives).
|
||||
- `hide` — Hides a shared drive from the default view. For more information, see [Manage shared drives](https://developers.google.com/workspace/drive/api/guides/manage-shareddrives).
|
||||
- `list` — Lists the user's shared drives. This method accepts the `q` parameter, which is a search query combining one or more search terms. For more information, see the [Search for shared drives](/workspace/drive/api/guides/search-shareddrives) guide.
|
||||
- `list` — Lists the user's shared drives. This method accepts the `q` parameter, which is a search query combining one or more search terms. For more information, see the [Search for shared drives](https://developers.google.com/workspace/drive/api/guides/search-shareddrives) guide.
|
||||
- `unhide` — Restores a shared drive to the default view. For more information, see [Manage shared drives](https://developers.google.com/workspace/drive/api/guides/manage-shareddrives).
|
||||
- `update` — Updates the metadata for a shared drive. For more information, see [Manage shared drives](https://developers.google.com/workspace/drive/api/guides/manage-shareddrives).
|
||||
|
||||
### files
|
||||
|
||||
- `copy` — Creates a copy of a file and applies any requested updates with patch semantics. For more information, see [Create and manage files](https://developers.google.com/workspace/drive/api/guides/create-file).
|
||||
- `create` — Creates a file. For more information, see [Create and manage files](/workspace/drive/api/guides/create-file). This method supports an */upload* URI and accepts uploaded media with the following characteristics: - *Maximum file size:* 5,120 GB - *Accepted Media MIME types:* `*/*` (Specify a valid MIME type, rather than the literal `*/*` value. The literal `*/*` is only used to indicate that any valid MIME type can be uploaded.
|
||||
- `create` — Creates a file. For more information, see [Create and manage files](https://developers.google.com/workspace/drive/api/guides/create-file). This method supports an */upload* URI and accepts uploaded media with the following characteristics: - *Maximum file size:* 5,120 GB - *Accepted Media MIME types:* `*/*` (Specify a valid MIME type, rather than the literal `*/*` value. The literal `*/*` is only used to indicate that any valid MIME type can be uploaded.
|
||||
- `download` — Downloads the content of a file. For more information, see [Download and export files](https://developers.google.com/workspace/drive/api/guides/manage-downloads). Operations are valid for 24 hours from the time of creation.
|
||||
- `export` — Exports a Google Workspace document to the requested MIME type and returns exported byte content. For more information, see [Download and export files](https://developers.google.com/workspace/drive/api/guides/manage-downloads). Note that the exported content is limited to 10 MB.
|
||||
- `generateIds` — Generates a set of file IDs which can be provided in create or copy requests. For more information, see [Create and manage files](https://developers.google.com/workspace/drive/api/guides/create-file).
|
||||
- `get` — Gets a file's metadata or content by ID. For more information, see [Search for files and folders](/workspace/drive/api/guides/search-files). If you provide the URL parameter `alt=media`, then the response includes the file contents in the response body. Downloading content with `alt=media` only works if the file is stored in Drive. To download Google Docs, Sheets, and Slides use [`files.export`](/workspace/drive/api/reference/rest/v3/files/export) instead.
|
||||
- `list` — Lists the user's files. For more information, see [Search for files and folders](/workspace/drive/api/guides/search-files). This method accepts the `q` parameter, which is a search query combining one or more search terms. This method returns *all* files by default, including trashed files. If you don't want trashed files to appear in the list, use the `trashed=false` query parameter to remove trashed files from the results.
|
||||
- `get` — Gets a file's metadata or content by ID. For more information, see [Search for files and folders](https://developers.google.com/workspace/drive/api/guides/search-files). If you provide the URL parameter `alt=media`, then the response includes the file contents in the response body. Downloading content with `alt=media` only works if the file is stored in Drive.
|
||||
- `list` — Lists the user's files. For more information, see [Search for files and folders](https://developers.google.com/workspace/drive/api/guides/search-files). This method accepts the `q` parameter, which is a search query combining one or more search terms. This method returns *all* files by default, including trashed files. If you don't want trashed files to appear in the list, use the `trashed=false` query parameter to remove trashed files from the results.
|
||||
- `listLabels` — Lists the labels on a file. For more information, see [List labels on a file](https://developers.google.com/workspace/drive/api/guides/list-labels).
|
||||
- `modifyLabels` — Modifies the set of labels applied to a file. For more information, see [Set a label field on a file](https://developers.google.com/workspace/drive/api/guides/set-label). Returns a list of the labels that were added or modified.
|
||||
- `update` — Updates a file's metadata, content, or both. When calling this method, only populate fields in the request that you want to modify. When updating fields, some fields might be changed automatically, such as `modifiedDate`. This method supports patch semantics. This method supports an */upload* URI and accepts uploaded media with the following characteristics: - *Maximum file size:* 5,120 GB - *Accepted Media MIME types:* `*/*` (Specify a valid MIME type, rather than the literal `*/*` value.
|
||||
|
||||
+34
-18
@@ -1,5 +1,11 @@
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
|
||||
const MAX_RETRIES: u32 = 3;
|
||||
/// Maximum seconds to sleep on a 429 Retry-After header. Prevents a hostile
|
||||
/// or misconfigured server from hanging the process indefinitely.
|
||||
const MAX_RETRY_DELAY_SECS: u64 = 60;
|
||||
const CONNECT_TIMEOUT_SECS: u64 = 10;
|
||||
|
||||
pub fn build_client() -> Result<reqwest::Client, crate::error::GwsError> {
|
||||
let mut headers = HeaderMap::new();
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
@@ -13,40 +19,50 @@ pub fn build_client() -> Result<reqwest::Client, crate::error::GwsError> {
|
||||
|
||||
reqwest::Client::builder()
|
||||
.default_headers(headers)
|
||||
.connect_timeout(std::time::Duration::from_secs(CONNECT_TIMEOUT_SECS))
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
crate::error::GwsError::Other(anyhow::anyhow!("Failed to build HTTP client: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
const MAX_RETRIES: u32 = 3;
|
||||
/// Maximum seconds to sleep on a 429 Retry-After header. Prevents a hostile
|
||||
/// or misconfigured server from hanging the process indefinitely.
|
||||
const MAX_RETRY_DELAY_SECS: u64 = 60;
|
||||
|
||||
/// Send an HTTP request with automatic retry on 429 (rate limit) responses.
|
||||
/// Send an HTTP request with automatic retry on 429 (rate limit) responses
|
||||
/// and transient connection/timeout errors.
|
||||
/// Respects the `Retry-After` header; falls back to exponential backoff (1s, 2s, 4s).
|
||||
pub async fn send_with_retry(
|
||||
build_request: impl Fn() -> reqwest::RequestBuilder,
|
||||
) -> Result<reqwest::Response, reqwest::Error> {
|
||||
let mut last_err: Option<reqwest::Error> = None;
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
let resp = build_request().send().await?;
|
||||
match build_request().send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status() != reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
if resp.status() != reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
return Ok(resp);
|
||||
let header_value = resp
|
||||
.headers()
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
let retry_after = compute_retry_delay(header_value, attempt);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
|
||||
}
|
||||
Err(e) if e.is_connect() || e.is_timeout() => {
|
||||
// Transient network error — retry with exponential backoff
|
||||
let delay = compute_retry_delay(None, attempt);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
|
||||
last_err = Some(e);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
let header_value = resp
|
||||
.headers()
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
let retry_after = compute_retry_delay(header_value, attempt);
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
|
||||
}
|
||||
|
||||
// Final attempt — return whatever we get
|
||||
build_request().send().await
|
||||
match build_request().send().await {
|
||||
Ok(resp) => Ok(resp),
|
||||
Err(e) => Err(last_err.unwrap_or(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the retry delay from a Retry-After header value and attempt number.
|
||||
|
||||
Reference in New Issue
Block a user