11import { cookie as HttpCookieAgentCookie , CookieAgent } from 'http-cookie-agent/undici' ;
22import queryString from 'query-string' ;
33import { Cookie , CookieJar } from 'tough-cookie' ;
4- import { Client , ProxyAgent } from 'undici' ;
4+ import undici , { Client , ProxyAgent } from 'undici' ;
55
66import { config } from '@/config' ;
77import ConfigNotFoundError from '@/errors/types/config-not-found' ;
@@ -136,8 +136,21 @@ export const twitterGot = async (
136136 )
137137 : { } ;
138138
139- const response = await ofetch . raw ( requestUrl , {
140- retry : 0 ,
139+ // Use undici.fetch directly instead of ofetch.raw to preserve the CookieAgent
140+ // dispatcher. Two layers drop it in the normal path:
141+ // 1. ofetch does not forward `dispatcher` to its internal fetch() call
142+ // 2. wrappedFetch (request-rewriter) does `new Request(input, init)` which
143+ // discards non-standard options like `dispatcher`
144+ // Additionally, setting `cookie` header manually doesn't work either because
145+ // the Fetch spec treats `cookie` as a forbidden header name, so
146+ // `new Request()` silently strips it.
147+ // The only way to send cookies via CookieAgent is to call undici.fetch with
148+ // the dispatcher option directly.
149+ //
150+ // Because undici.fetch is the standard Fetch API and does not support ofetch's
151+ // `onResponse` callback, the rate-limit and auth error handling that was
152+ // previously in `onResponse` is now inlined below.
153+ const response = await undici . fetch ( requestUrl , {
141154 headers : {
142155 authority : 'x.com' ,
143156 accept : '*/*' ,
@@ -160,67 +173,77 @@ export const twitterGot = async (
160173 } ) ,
161174 } ,
162175 dispatcher : dispatchers ?. agent ,
163- onResponse : async ( { response } ) => {
164- const remaining = response . headers . get ( 'x-rate-limit-remaining' ) ;
165- const remainingInt = Number . parseInt ( remaining || '0' ) ;
166- const reset = response . headers . get ( 'x-rate-limit-reset' ) ;
167- logger . debug (
168- `twitter debug: twitter rate limit remaining for token ${ auth ?. token } is ${ remaining } and reset at ${ reset } , auth: ${ JSON . stringify ( auth ) } , status: ${ response . status } , data: ${ JSON . stringify ( response . _data ?. data ) } , cookie: ${ JSON . stringify ( dispatchers ?. jar . serializeSync ( ) ) } `
169- ) ;
170- if ( auth ) {
171- if ( remaining && remainingInt < 2 && reset ) {
172- const resetTime = new Date ( Number . parseInt ( reset ) * 1000 ) ;
173- const delay = ( resetTime . getTime ( ) - Date . now ( ) ) / 1000 ;
174- logger . debug ( `twitter debug: twitter rate limit exceeded for token ${ auth . token } with status ${ response . status } , will unlock after ${ delay } s` ) ;
175- await cache . set ( `${ lockPrefix } ${ auth . token } ` , '1' , Math . ceil ( delay ) * 2 ) ;
176- } else if ( response . status === 429 || JSON . stringify ( response . _data ?. data ) === '{"user":{}}' ) {
177- logger . debug ( `twitter debug: twitter rate limit exceeded for token ${ auth . token } with status ${ response . status } ` ) ;
178- await cache . set ( `${ lockPrefix } ${ auth . token } ` , '1' , 2000 ) ;
179- } else if ( response . status === 403 || response . status === 401 ) {
180- const newCookie = await login ( {
181- username : auth . username ,
182- password : auth . password ,
183- authenticationSecret : auth . authenticationSecret ,
184- } ) ;
185- if ( newCookie ) {
186- logger . debug ( `twitter debug: reset twitter cookie for token ${ auth . token } , ${ newCookie } ` ) ;
187- await cache . set ( `twitter:cookie:${ auth . token } ` , newCookie , config . cache . contentExpire ) ;
188- logger . debug ( `twitter debug: unlock twitter cookie for token ${ auth . token } with error1` ) ;
189- await cache . set ( `${ lockPrefix } ${ auth . token } ` , '' , 1 ) ;
190- } else {
191- const tokenIndex = config . twitter . authToken ?. indexOf ( auth . token ) ;
192- if ( tokenIndex !== undefined && tokenIndex !== - 1 ) {
193- config . twitter . authToken ?. splice ( tokenIndex , 1 ) ;
194- }
195- if ( auth . username ) {
196- const usernameIndex = config . twitter . username ?. indexOf ( auth . username ) ;
197- if ( usernameIndex !== undefined && usernameIndex !== - 1 ) {
198- config . twitter . username ?. splice ( usernameIndex , 1 ) ;
199- }
200- }
201- if ( auth . password ) {
202- const passwordIndex = config . twitter . password ?. indexOf ( auth . password ) ;
203- if ( passwordIndex !== undefined && passwordIndex !== - 1 ) {
204- config . twitter . password ?. splice ( passwordIndex , 1 ) ;
205- }
206- }
207- logger . debug ( `twitter debug: delete twitter cookie for token ${ auth . token } with status ${ response . status } , remaining tokens: ${ config . twitter . authToken ?. length } ` ) ;
208- await cache . set ( `${ lockPrefix } ${ auth . token } ` , '1' , 3600 ) ;
176+ } ) ;
177+
178+ let responseData : any ;
179+ try {
180+ responseData = await response . json ( ) ;
181+ } catch {
182+ responseData = null ;
183+ }
184+
185+ // Handle rate limiting and auth errors
186+ const remaining = response . headers . get ( 'x-rate-limit-remaining' ) ;
187+ const remainingInt = Number . parseInt ( remaining || '0' ) ;
188+ const reset = response . headers . get ( 'x-rate-limit-reset' ) ;
189+ logger . debug (
190+ `twitter debug: twitter rate limit remaining for token ${ auth ?. token } is ${ remaining } and reset at ${ reset } , auth: ${ JSON . stringify ( auth ) } , status: ${ response . status } , data: ${ JSON . stringify ( responseData ?. data ) } , cookie: ${ JSON . stringify ( dispatchers ?. jar . serializeSync ( ) ) } `
191+ ) ;
192+ if ( auth ) {
193+ if ( remaining && remainingInt < 2 && reset ) {
194+ const resetTime = new Date ( Number . parseInt ( reset ) * 1000 ) ;
195+ const delay = ( resetTime . getTime ( ) - Date . now ( ) ) / 1000 ;
196+ logger . debug ( `twitter debug: twitter rate limit exceeded for token ${ auth . token } with status ${ response . status } , will unlock after ${ delay } s` ) ;
197+ await cache . set ( `${ lockPrefix } ${ auth . token } ` , '1' , Math . ceil ( delay ) * 2 ) ;
198+ } else if ( response . status === 429 || JSON . stringify ( responseData ?. data ) === '{"user":{}}' ) {
199+ logger . debug ( `twitter debug: twitter rate limit exceeded for token ${ auth . token } with status ${ response . status } ` ) ;
200+ await cache . set ( `${ lockPrefix } ${ auth . token } ` , '1' , 2000 ) ;
201+ } else if ( response . status === 403 || response . status === 401 ) {
202+ const newCookie = await login ( {
203+ username : auth . username ,
204+ password : auth . password ,
205+ authenticationSecret : auth . authenticationSecret ,
206+ } ) ;
207+ if ( newCookie ) {
208+ logger . debug ( `twitter debug: reset twitter cookie for token ${ auth . token } , ${ newCookie } ` ) ;
209+ await cache . set ( `twitter:cookie:${ auth . token } ` , newCookie , config . cache . contentExpire ) ;
210+ await cache . set ( `${ lockPrefix } ${ auth . token } ` , '' , 1 ) ;
211+ } else {
212+ const tokenIndex = config . twitter . authToken ?. indexOf ( auth . token ) ;
213+ if ( tokenIndex !== undefined && tokenIndex !== - 1 ) {
214+ config . twitter . authToken ?. splice ( tokenIndex , 1 ) ;
215+ }
216+ if ( auth . username ) {
217+ const usernameIndex = config . twitter . username ?. indexOf ( auth . username ) ;
218+ if ( usernameIndex !== undefined && usernameIndex !== - 1 ) {
219+ config . twitter . username ?. splice ( usernameIndex , 1 ) ;
220+ }
221+ }
222+ if ( auth . password ) {
223+ const passwordIndex = config . twitter . password ?. indexOf ( auth . password ) ;
224+ if ( passwordIndex !== undefined && passwordIndex !== - 1 ) {
225+ config . twitter . password ?. splice ( passwordIndex , 1 ) ;
209226 }
210- } else {
211- logger . debug ( `twitter debug: unlock twitter cookie with success for token ${ auth . token } ` ) ;
212- await cache . set ( `${ lockPrefix } ${ auth . token } ` , '' , 1 ) ;
213227 }
228+ logger . debug ( `twitter debug: delete twitter cookie for token ${ auth . token } with status ${ response . status } , remaining tokens: ${ config . twitter . authToken ?. length } ` ) ;
229+ await cache . set ( `${ lockPrefix } ${ auth . token } ` , '1' , 3600 ) ;
214230 }
215- } ,
216- } ) ;
231+ } else {
232+ logger . debug ( `twitter debug: unlock twitter cookie with success for token ${ auth . token } ` ) ;
233+ await cache . set ( `${ lockPrefix } ${ auth . token } ` , '' , 1 ) ;
234+ }
235+ }
236+
237+ if ( response . status >= 400 ) {
238+ throw new Error ( `Twitter API error: ${ response . status } ` ) ;
239+ }
217240
218241 if ( auth ?. token ) {
219242 logger . debug ( `twitter debug: update twitter cookie for token ${ auth . token } ` ) ;
220243 await cache . set ( `twitter:cookie:${ auth . token } ` , JSON . stringify ( dispatchers ?. jar . serializeSync ( ) ) , config . cache . contentExpire ) ;
221244 }
222245
223- return response . _data ;
246+ return responseData ;
224247} ;
225248
226249export const paginationTweets = async ( endpoint : string , userId : number | undefined , variables : Record < string , any > , path ?: string [ ] ) => {
0 commit comments