AlistGo/alist · error

access_token expired: provide a refresh_token together with

Error message

access_token expired: provide a refresh_token together with clientID/clientSecret, or switch to client_credentials mode

What it means

Returned when the HTTP GET for an strm link's URL (after optional absolutization) completes but returns a status >= 400. The transport worked; the remote origin refused the request (401/403 auth, 404 gone, 410 removed, 429 throttled, 5xx broken).

Source

Thrown at drivers/123_open/client.go:19

package _123Open

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/alist-org/alist/v3/internal/conf"
	"github.com/alist-org/alist/v3/internal/op"
	pan123 "github.com/okatu-loli/go-123pan"
)

// tokenRefreshMargin is how long before expiry a token is proactively renewed.
const tokenRefreshMargin = 10 * time.Minute

var errNoRefreshCredentials = errors.New("access_token expired: provide a refresh_token together with clientID/clientSecret, or switch to client_credentials mode")

// newSDKClient builds the SDK client for the configured authentication mode.
func (d *Open123) newSDKClient() (*pan123.Client, error) {
	opts := []pan123.Option{
		pan123.WithHTTPClient(&http.Client{Timeout: 60 * time.Second}),
		pan123.WithUserAgent("AList/" + conf.Version),
	}
	switch d.AuthMode {
	case AuthToken:
		if d.AccessToken == "" {
			return nil, errors.New("access_token is required in token mode")
		}
		c := pan123.NewWithToken(d.AccessToken, opts...)
		// expiry is unknown for an externally issued token; refresh on demand
		c.SetToken(d.AccessToken, d.tokenExpiry())
		return c, nil
	case AuthClientCredentials, "":
		if d.ClientID == "" || d.ClientSecret == "" {

View on GitHub (pinned to 843d9dc814)

Solutions

  1. Verify the URL in the .strm file still resolves in a browser or curl with the same headers.
  2. For 401/403, add the required credentials/headers to the link's Header so the fetch includes them, or fix the target's access settings.
  3. For 429/5xx, retry with backoff and reduce concurrent stream opens.
  4. Regenerate the .strm file against the current location of the media.

Example fix

// before: plain link, origin returns 403
link := &model.Link{URL: u}

// after: propagate needed auth header
link := &model.Link{
	URL:    u,
	Header: http.Header{"Authorization": []string{"Bearer " + token}},
}
Defensive patterns

Strategy: retry

Validate before calling

// optional pre-flight availability check
if resp, err := base.RestyClient.R().SetContext(ctx).Head(link.URL); err == nil && resp.StatusCode() >= 400 {
	// do not attempt full read; surface broken-link error early
}

Type guard

func isBrokenStatusErr(err error) bool {
	return err != nil && strings.Contains(err.Error(), "read url failed: status=")
}

Try / catch

err := readStrmTarget(ctx, link)
if isBrokenStatusErr(err) {
	if isTransientStatus(err) {
		// retry with backoff (429/5xx)
	} else {
		// mark strm entry stale; do not retry (401/403/404/410)
	}
}

Prevention

When it happens

Trigger: Fetching content of a .strm target whose backing file was moved or deleted (404), requires expired credentials (401/403), rate limits the client (429), or the origin server errors (5xx).

Common situations: Expired signed URLs inside .strm files; media servers moved/renamed their paths; credentials rotated without regenerating .strm files; aggressive parallel opens hitting origin rate limits.

Understand the failure class

Related errors


AI-assisted analysis of AlistGo/alist@843d9dc814 (2026-08-15). Data as JSON: /api/errors/f6cb563c6b67fc5f. Report an issue: GitHub.