[bugfix] set link header lo,hi values directly from returned slice, don't account for filtering (#4421)

this fixes an issue with list pagination in list timelines as seen here: https://codeberg.org/tusky/Tusky/issues/5235

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4421
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-09-11 14:57:32 +02:00 committed by kim
commit 33fed81a8d
2 changed files with 19 additions and 29 deletions

View file

@ -387,10 +387,6 @@ func (t *StatusTimeline) Load(
return nil, "", "", gtserror.Newf("error loading statuses: %w", err)
}
// Set returned lo, hi values.
lo = metas[len(metas)-1].ID
hi = metas[0].ID
// Prepare frontend API models for
// the cached statuses. For now this
// also does its own extra filtering.
@ -408,8 +404,8 @@ func (t *StatusTimeline) Load(
// we need to call to database.
if len(apiStatuses) < limit {
// Pass through to main timeline db load function.
apiStatuses, lo, hi, err = loadStatusTimeline(ctx,
// Pass to main timeline db load function.
apiStatuses, err = loadStatusTimeline(ctx,
nextPg,
metas,
apiStatuses,
@ -422,6 +418,15 @@ func (t *StatusTimeline) Load(
}
}
// Reset values.
lo, hi = "", ""
if len(apiStatuses) > 0 {
// Set returned lo, hi paging values.
lo = apiStatuses[len(apiStatuses)-1].ID
hi = apiStatuses[0].ID
}
if order.Ascending() {
// The caller always expects the statuses
// to be returned in DESC order, but we
@ -452,18 +457,12 @@ func loadStatusTimeline(
prepareAPI func(status *gtsmodel.Status) (apiStatus *apimodel.Status, err error),
) (
[]*apimodel.Status,
string, // lo
string, // hi
error,
) {
if loadPage == nil {
panic("nil load page func")
}
// Lowest and highest ID
// vals of loaded statuses.
var lo, hi string
// Extract paging params, in particular
// limit is used separate to nextPg to
// determine the *expected* return limit,
@ -489,7 +488,7 @@ func loadStatusTimeline(
// Load next timeline statuses.
statuses, err := loadPage(nextPg)
if err != nil {
return nil, "", "", gtserror.Newf("error loading timeline: %w", err)
return nil, gtserror.Newf("error loading timeline: %w", err)
}
// No more statuses from
@ -498,12 +497,6 @@ func loadStatusTimeline(
break
}
if hi == "" {
// Set hi returned paging
// value if not already set.
hi = statuses[0].ID
}
// Update nextPg cursor parameter for next database query.
nextPageParams(nextPg, statuses[len(statuses)-1].ID, order)
@ -530,13 +523,7 @@ func loadStatusTimeline(
)
}
if len(apiStatuses) > 0 {
// We finished loading statuses with
// values to return, set lo paging value.
lo = apiStatuses[len(apiStatuses)-1].ID
}
return apiStatuses, lo, hi, nil
return apiStatuses, nil
}
// InsertOne allows you to insert a single status into the timeline, with optional prepared API model.
@ -671,6 +658,9 @@ func (t *StatusTimeline) UnprepareByStatusIDs(statusIDs ...string) {
// timeline entries authored by account ID, including boosts by account ID.
func (t *StatusTimeline) UnprepareByAccountIDs(accountIDs ...string) {
keys := make([]structr.Key, len(accountIDs))
if len(keys) != len(accountIDs) {
panic(gtserror.New("BCE"))
}
// Nil check indices outside loops.
if t.idx_AccountID == nil ||

View file

@ -91,9 +91,9 @@ func (suite *PublicTestSuite) TestPublicTimelineGetNotEmpty() {
// some other statuses were filtered out.
suite.NoError(errWithCode)
suite.Len(resp.Items, 1)
suite.Equal(`<http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel="next", <http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01FF25D5Q0DH7CHD57CTRS6WK0>; rel="prev"`, resp.LinkHeader)
suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV`, resp.NextLink)
suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01FF25D5Q0DH7CHD57CTRS6WK0`, resp.PrevLink)
suite.Equal("<http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel=\"next\", <http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel=\"prev\"", resp.LinkHeader)
suite.Equal("http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV", resp.NextLink)
suite.Equal("http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01F8MHCP5P2NWYQ416SBA0XSEV", resp.PrevLink)
}
// A timeline containing a status hidden due to filtering should return other statuses with no error.