cloudreve/cloudreve · warning

failed to unmarshal page token: %w

Error message

failed to unmarshal page token: %w

What it means

Second failure mode of pageTokenFromString: the string decodes as base64 but the bytes are not valid JSON for the PageToken struct (fields: time, id, string, int, start_with_file). Because PageToken is a struct (not a map), json.Unmarshal rejects syntactically invalid JSON immediately; semantically odd but well-formed JSON is accepted with unknown fields ignored. The ID field is tagged json:"-" so tokens minted by hand-crafting JSON cannot carry a raw numeric ID — only the hashid field 'id' is honored.

Source

Thrown at inventory/common.go:60

const (
	OrderDirectionAsc  = OrderDirection("asc")
	OrderDirectionDesc = OrderDirection("desc")
)

var (
	ErrTooManyArguments = fmt.Errorf("too many arguments")
)

func pageTokenFromString(s string, hasher hashid.Encoder, idType int) (*PageToken, error) {
	sB64Decoded, err := base64.StdEncoding.DecodeString(s)
	if err != nil {
		return nil, fmt.Errorf("failed to decode base64 for page token: %w", err)
	}

	token := &PageToken{}
	if err := json.Unmarshal(sB64Decoded, token); err != nil {
		return nil, fmt.Errorf("failed to unmarshal page token: %w", err)
	}

	id, err := hasher.Decode(token.IDHash, idType)
	if err != nil {
		return nil, fmt.Errorf("failed to decode id: %w", err)
	}

	if token.Time == nil {
		token.Time = &time.Time{}
	}

	token.ID = id
	return token, nil
}

func (p *PageToken) Encode(hasher hashid.Encoder, encodeFunc hashid.EncodeFunc) (string, error) {
	p.IDHash = encodeFunc(hasher, p.ID)
	res, err := json.Marshal(p)

View on GitHub (pinned to 20c95ad73f)

Solutions

  1. Only use tokens issued by the same deployment in the same API version: take next_token from the previous response and feed it back verbatim.
  2. Pre-validate client-side: base64-decode then json.Valid(bytes) before sending; if invalid, restart from page one.
  3. Do not cache page tokens across upgrades; after a Cloudreve upgrade, drop stored tokens and re-list from the beginning.
  4. If you maintain a fork that changed PageToken fields, version the token (e.g. prefix v1:) so mismatches are detectable.

Example fix

// before: hand-crafted token
raw, _ := json.Marshal(map[string]any{"page": 3})
token := base64.StdEncoding.EncodeToString(raw)
res, err := client.List(ctx, &inventory.ListDavAccountArgs{PageToken: token})

// after: use the server-issued cursor only
res, err := client.List(ctx, &inventory.ListDavAccountArgs{PageToken: prevRes.PaginationResults.NextPageToken})
Defensive patterns

Strategy: validation

Validate before calling

// Full client-side structural pre-check
func validPageToken(s string) bool {
    raw, err := base64.StdEncoding.DecodeString(s)
    if err != nil || !json.Valid(raw) {
        return false
    }
    var probe map[string]any
    return json.Unmarshal(raw, &probe) == nil
}

if args.PageToken != "" && !validPageToken(args.PageToken) {
    args.PageToken = ""
}

Type guard

func isPageTokenShapeError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "failed to unmarshal page token")
}

Try / catch

if err != nil && isPageTokenShapeError(err) {
    return apiError(400, "stale or malformed page token; restart pagination")
}

Prevention

When it happens

Trigger: Passing base64 of a non-JSON payload ("aGVsbG8=" for "hello"), a JSON array, or a JSON object from a different application's pagination scheme; tokens generated by a different Cloudreve version whose PageToken JSON schema is incompatible; hand-constructed tokens attempting to bypass cursor pagination.

Common situations: Integrations that reuse another service's opaque-cursor format; scripts that base64-encode arbitrary JSON hoping to synthesize a page pointer; version skew between the token issuer and consumer (e.g. token cached long-term across an upgrade that changed token fields).

Related errors


AI-assisted analysis of cloudreve/cloudreve@20c95ad73f (2026-08-16). Data as JSON: /api/errors/276619fef11732ea. Report an issue: GitHub.