fix: add cursor pagination support to REST fallback for followers/following

When GraphQL endpoints return 404 and fall back to REST API v1.1,
pagination now works correctly by:
- Accepting cursor parameter in getFollowersViaRest and getFollowingViaRest
- Passing cursor to REST API requests
- Extracting next_cursor_str from REST responses
- Returning nextCursor in the result for proper --all pagination
This commit is contained in:
Henry Lee
2026-06-24 18:42:16 +09:00
committed by yuta
parent df449ea736
commit 058aa0e3c3
+18 -8
View File
@@ -65,7 +65,7 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
.filter((u) => u !== null); .filter((u) => u !== null);
} }
private async getFollowersViaRest(userId: string, count: number): Promise<FollowingResult> { private async getFollowersViaRest(userId: string, count: number, cursor?: string): Promise<FollowingResult> {
const params = new URLSearchParams({ const params = new URLSearchParams({
user_id: userId, user_id: userId,
count: String(count), count: String(count),
@@ -73,6 +73,10 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
include_user_entities: 'false', include_user_entities: 'false',
}); });
if (cursor) {
params.set('cursor', cursor);
}
const urls = [ const urls = [
`https://x.com/i/api/1.1/followers/list.json?${params.toString()}`, `https://x.com/i/api/1.1/followers/list.json?${params.toString()}`,
`https://api.twitter.com/1.1/followers/list.json?${params.toString()}`, `https://api.twitter.com/1.1/followers/list.json?${params.toString()}`,
@@ -95,11 +99,13 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
const data = (await response.json()) as { const data = (await response.json()) as {
users?: RestUser[]; users?: RestUser[];
next_cursor_str?: string;
}; };
const users = this.parseUsersFromRestResponse(data.users); const users = this.parseUsersFromRestResponse(data.users);
const nextCursor = data.next_cursor_str && data.next_cursor_str !== '0' ? data.next_cursor_str : undefined;
return { success: true, users }; return { success: true, users, nextCursor };
} catch (error) { } catch (error) {
lastError = error instanceof Error ? error.message : String(error); lastError = error instanceof Error ? error.message : String(error);
} }
@@ -108,7 +114,7 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
return { success: false, error: lastError ?? 'Unknown error fetching followers' }; return { success: false, error: lastError ?? 'Unknown error fetching followers' };
} }
private async getFollowingViaRest(userId: string, count: number): Promise<FollowingResult> { private async getFollowingViaRest(userId: string, count: number, cursor?: string): Promise<FollowingResult> {
const params = new URLSearchParams({ const params = new URLSearchParams({
user_id: userId, user_id: userId,
count: String(count), count: String(count),
@@ -116,6 +122,10 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
include_user_entities: 'false', include_user_entities: 'false',
}); });
if (cursor) {
params.set('cursor', cursor);
}
const urls = [ const urls = [
`https://x.com/i/api/1.1/friends/list.json?${params.toString()}`, `https://x.com/i/api/1.1/friends/list.json?${params.toString()}`,
`https://api.twitter.com/1.1/friends/list.json?${params.toString()}`, `https://api.twitter.com/1.1/friends/list.json?${params.toString()}`,
@@ -138,11 +148,13 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
const data = (await response.json()) as { const data = (await response.json()) as {
users?: RestUser[]; users?: RestUser[];
next_cursor_str?: string;
}; };
const users = this.parseUsersFromRestResponse(data.users); const users = this.parseUsersFromRestResponse(data.users);
const nextCursor = data.next_cursor_str && data.next_cursor_str !== '0' ? data.next_cursor_str : undefined;
return { success: true, users }; return { success: true, users, nextCursor };
} catch (error) { } catch (error) {
lastError = error instanceof Error ? error.message : String(error); lastError = error instanceof Error ? error.message : String(error);
} }
@@ -333,8 +345,7 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
// GraphQL Following can also return 404 (queryId churn / endpoint flakiness). // GraphQL Following can also return 404 (queryId churn / endpoint flakiness).
// Fallback to the internal v1.1 REST endpoint used by the web client through the relay. // Fallback to the internal v1.1 REST endpoint used by the web client through the relay.
// Note: REST fallback does not support cursor pagination. const restAttempt = await this.getFollowingViaRest(userId, count, cursor);
const restAttempt = await this.getFollowingViaRest(userId, count);
if (restAttempt.success) { if (restAttempt.success) {
return restAttempt; return restAttempt;
} }
@@ -439,8 +450,7 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
// GraphQL Followers regularly returns 404 (queryId churn / endpoint flakiness). // GraphQL Followers regularly returns 404 (queryId churn / endpoint flakiness).
// Fallback to the internal v1.1 REST endpoint used by the web client through the relay. // Fallback to the internal v1.1 REST endpoint used by the web client through the relay.
// Note: REST fallback does not support cursor pagination. const restAttempt = await this.getFollowersViaRest(userId, count, cursor);
const restAttempt = await this.getFollowersViaRest(userId, count);
if (restAttempt.success) { if (restAttempt.success) {
return restAttempt; return restAttempt;
} }