Skip to content

Bug: gmail +watch and events +subscribe exit after ~1 hour due to expired access token #392

@meme8383

Description

@meme8383

Description

The long-running helper commands gmail +watch and events +subscribe obtain OAuth2 access tokens once at startup and pass them as static &str values into their pull loops. Since Google access tokens expire after 3600 seconds (1 hour), the Pub/Sub pull request eventually receives a 401 response, which the loop treats as a fatal error and exits.

Steps to reproduce

gws gmail +watch \
  --subscription projects/my-project/subscriptions/my-sub \
  --label-ids INBOX \
  --poll-interval 5

Wait ~60 minutes. The process exits with status 1.

Expected behavior

The +watch command should run indefinitely (up to the 7-day Gmail watch expiration), automatically refreshing the access token before or when it expires.

Actual behavior

The process exits with a non-zero status after ~1 hour on every run. The lifetime is consistent across restarts, matching the Google OAuth2 access token TTL of 3600 seconds.

Root cause

In src/helpers/gmail/watch.rs, tokens are obtained once before the loop:

// watch.rs lines 17-22
let gmail_token = auth::get_token(&[GMAIL_SCOPE]).await;
let pubsub_token = auth::get_token(&[PUBSUB_SCOPE]).await;

auth::get_token() (in src/auth.rs line 91) builds a yup_oauth2 authenticator, calls .token() once to get the access token as a String, then drops the authenticator, discarding its refresh capability.

The tokens are passed into watch_pull_loop() as &str:

// watch.rs lines 247-254
async fn watch_pull_loop(
    client: &reqwest::Client,
    pubsub_token: &str,
    gmail_token: &str,

When the token expires after 3600s, the next Pub/Sub pull returns a 401, which hits the fatal error path at line 282-289:

if !resp.status().is_success() {
    let body = resp.text().await.unwrap_or_default();
    return Err(GwsError::Api {});
}

The same pattern exists in src/helpers/events/subscribe.rs (pull_loop takes token: &str).

The HTTP client (src/client.rs) is a plain reqwest::Client with no auth middleware.

Suggested fix

Retain the yup_oauth2 authenticator and call .token() before each API request (or on a timer), rather than extracting the access token string once.

Alternatively, get_token() could be called periodically inside the loop (e.g., every 45 minutes or upon receiving a 401).

Affected commands

  • gws gmail +watch (src/helpers/gmail/watch.rs)
  • gws events +subscribe (src/helpers/events/subscribe.rs)
  • Potentially any future long-running helper that calls auth::get_token() once

Version

gws 0.8.0

Environment

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions