summaryrefslogtreecommitdiff
path: root/internal/api/google_tasks_oauth.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/google_tasks_oauth.go')
-rw-r--r--internal/api/google_tasks_oauth.go27
1 files changed, 23 insertions, 4 deletions
diff --git a/internal/api/google_tasks_oauth.go b/internal/api/google_tasks_oauth.go
index 1f5f8c5..6edba21 100644
--- a/internal/api/google_tasks_oauth.go
+++ b/internal/api/google_tasks_oauth.go
@@ -3,6 +3,7 @@ package api
import (
"context"
"fmt"
+ "time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
@@ -45,12 +46,14 @@ func GoogleTasksOAuthConfig(clientID, clientSecret, redirectURL string) *oauth2.
// call -- cheap enough to just hit the DB each time rather than add a
// second layer of in-process caching.
type dbTokenSource struct {
- ctx context.Context
store TokenStore
source string
config *oauth2.Config
}
+// tokenRefreshTimeout bounds a single refresh round-trip to Google.
+const tokenRefreshTimeout = 30 * time.Second
+
func (d *dbTokenSource) Token() (*oauth2.Token, error) {
current, err := d.store.GetOAuthToken(d.source)
if err != nil {
@@ -60,7 +63,19 @@ func (d *dbTokenSource) Token() (*oauth2.Token, error) {
return nil, fmt.Errorf("google tasks not connected -- visit /settings and connect it")
}
- refreshed, err := d.config.TokenSource(d.ctx, current).Token()
+ // Deliberately NOT a context captured at construction time. main.go builds
+ // the client with a short-lived init context and cancels it the moment
+ // construction returns; reusing that here meant the first refresh after the
+ // startup access token aged out — roughly an hour in — failed with
+ // "context canceled", and so did every one after it. Google Tasks was
+ // broken from that point until a restart, which bought another hour.
+ //
+ // A token refresh is not logically part of whichever request happened to
+ // trigger it, so it gets its own background context with its own bound.
+ ctx, cancel := context.WithTimeout(context.Background(), tokenRefreshTimeout)
+ defer cancel()
+
+ refreshed, err := d.config.TokenSource(ctx, current).Token()
if err != nil {
return nil, fmt.Errorf("refreshing token: %w", err)
}
@@ -87,7 +102,11 @@ func (d *dbTokenSource) Token() (*oauth2.Token, error) {
// succeeds; API calls fail with the dbTokenSource error above until a
// token exists (see HandleGoogleTasksOAuthCallback).
func NewGoogleTasksOAuthClient(ctx context.Context, oauthConfig *oauth2.Config, tokenStore TokenStore, tasklistID, timezone string) (*GoogleTasksClient, error) {
- src := &dbTokenSource{ctx: ctx, store: tokenStore, source: GoogleTasksOAuthSource, config: oauthConfig}
- httpClient := oauth2.NewClient(ctx, src)
+ src := &dbTokenSource{store: tokenStore, source: GoogleTasksOAuthSource, config: oauthConfig}
+ // context.Background(), not ctx: oauth2.NewClient binds the context it is
+ // given to the returned client's transport for its whole lifetime, and ctx
+ // here is the caller's short-lived init context. The per-call context still
+ // applies — it comes from the tasks service methods themselves.
+ httpClient := oauth2.NewClient(context.Background(), src)
return NewGoogleTasksClient(ctx, option.WithHTTPClient(httpClient), tasklistID, timezone)
}