From d64238470e6c4d1d7acebec69b1e7c6951baba7c Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Thu, 24 Sep 2026 23:10:31 -0500 Subject: [PATCH] decode URL-safe job list cursors `JobListCursor.MarshalText` emits URL-safe base64, but the decoder uses the standard alphabet. A job kind that puts `-` or `_` in the encoded text makes a cursor produced by River fail to round-trip. Decode with the same URL-safe alphabet used for encoding. Cover a cursor whose ASCII job kind produces `-` in its text representation. --- CHANGELOG.md | 1 + job_list_params.go | 4 ++-- job_list_params_test.go | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd23289..f5aeffcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed `JobListCursor.UnmarshalText` rejecting valid cursors whose URL-safe base64 encoding contains `-` or `_`, so cursors produced by `MarshalText` always round-trip. [PR #1388](https://github.com/riverqueue/river/pull/1388). - Fixed SQLite job list pagination skipping or repeating jobs by formatting cursor timestamps consistently with stored timestamps. [PR #1374](https://github.com/riverqueue/river/pull/1374). - Improved PostgreSQL job listing performance when filtering by one finalized state (`completed`, `cancelled`, or `discarded`) and sorting by finalized time, including in River UI. [PR #1374](https://github.com/riverqueue/river/pull/1374). - Fixed `JobRescuer` overwriting jobs that complete, leave the running state, or are claimed again by another worker after being fetched for rescue, preserving their state, errors, metadata, and timestamps across PostgreSQL and SQLite drivers. Fixes [#1302](https://github.com/riverqueue/river/issues/1302). [PR #1373](https://github.com/riverqueue/river/pull/1373). diff --git a/job_list_params.go b/job_list_params.go index ded6e878..ddee1632 100644 --- a/job_list_params.go +++ b/job_list_params.go @@ -64,8 +64,8 @@ func jobListCursorFromJobAndParams(job *rivertype.JobRow, listParams *JobListPar // UnmarshalText implements encoding.TextUnmarshaler to decode the cursor from // a previously marshaled string. func (c *JobListCursor) UnmarshalText(text []byte) error { - dst := make([]byte, base64.StdEncoding.DecodedLen(len(text))) - n, err := base64.StdEncoding.Decode(dst, text) + dst := make([]byte, base64.URLEncoding.DecodedLen(len(text))) + n, err := base64.URLEncoding.Decode(dst, text) if err != nil { return err } diff --git a/job_list_params_test.go b/job_list_params_test.go index 5672866b..12089371 100644 --- a/job_list_params_test.go +++ b/job_list_params_test.go @@ -189,6 +189,25 @@ func Test_JobListCursor_MarshalJSON(t *testing.T) { _, err := json.Marshal(cursor) require.EqualError(t, err, "json: error calling MarshalText for type *river.JobListCursor: cursor initialized with only a job can't be marshaled; try a cursor from JobListResult instead") }) + + t.Run("URLSafeAlphabet", func(t *testing.T) { + t.Parallel() + + cursor := &JobListCursor{ + id: 1, + kind: "a~", + queue: "default", + sortField: JobListOrderByID, + } + + text, err := cursor.MarshalText() + require.NoError(t, err) + require.Contains(t, string(text), "-") + + decoded := &JobListCursor{} + require.NoError(t, decoded.UnmarshalText(text)) + require.Equal(t, cursor, decoded) + }) } func Test_JobListParams_toDBParams(t *testing.T) {