Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token expired #514

Closed
Ph-St opened this issue Jan 13, 2025 · 13 comments
Closed

Token expired #514

Ph-St opened this issue Jan 13, 2025 · 13 comments

Comments

@Ph-St
Copy link

Ph-St commented Jan 13, 2025

According to the documentation, the SDK should take care of token management and refresh tokens before they expire. Nevertheless, I'm getting lots of "Token expired" errors, such as this:

atproto_client.exceptions.BadRequestError: Response(success=False, status_code=400, content=XrpcError(error='ExpiredToken', message='Token has been revoked'), headers={'x-powered-by': 'Express', 'access-control-allow-origin': '*', 'cache-control': 'private', 'vary': 'Authorization, Accept-Encoding', 'ratelimit-limit': '3000', 'ratelimit-remaining': '2925', 'ratelimit-reset': '1736759405', 'ratelimit-policy': '3000;w=300', 'content-type': 'application/json; charset=utf-8', 'content-length': '59', 'etag': 'W/"3b-elKlVAnBs8eAB241ckLlScv0EUs"', 'date': 'Mon, 13 Jan 2025 09:09:25 GMT', 'keep-alive': 'timeout=90', 'strict-transport-security': 'max-age=63072000'})

My script runs every 30 minutes, just makes a few API calls and I'm saving and reusing sessions as suggested in the documentation. Still, I'm getting errors like the above. Any idea what I could be doing wrong?

@emilyhunt
Copy link

Could you post the code that you're using to save and reuse sessions?

For reference, this is the implementation of sessions in the Astronomy feeds, and I don't think we've ever had this issue. It's a bit bigger than the example in the ATProto SDK docs as it also saves to a file.

@MarshalX
Copy link
Owner

MarshalX commented Jan 13, 2025

Token has been revoked

Do you happen to change or delete app passwords? do you have only 1 script? Do you save your session in a disc file? do you use the same file with the saved session from many places?

@Ph-St
Copy link
Author

Ph-St commented Jan 13, 2025

Thank you both for your answers.

@emilyhunt: I used the code provided in the respective example in this repo.

@MarshalX: I never changed or deleted the app password since I created it. I have in fact 2 scripts. Both are using the session reuse system from the example you provided but with different session files. One script runs every 30 minutes, the other is continuously running and sleeps in-between calls. The calls here are quite a lot actually (looking for unread messages every 2 secs) but the rate-limit doesn't seem to be the problem, as the error message states that there's quite a lot remaining.

As a test I just deleted the session file and then the script worked. So I suppose it saved a session with a revoked token?

@MarshalX
Copy link
Owner

@Ph-St do you use different app passwords for each script?

@Ph-St
Copy link
Author

Ph-St commented Jan 13, 2025

@MarshalX No, they use the same app password. Should I create one for each instead?

@MarshalX
Copy link
Owner

@Ph-St you could try. I am not sure how backend handles many access tokens for one app password

@Ph-St
Copy link
Author

Ph-St commented Jan 13, 2025

@MarshalX OK, I will try and report back.

@Ph-St
Copy link
Author

Ph-St commented Jan 13, 2025

@MarshalX Just checked the log and there are fresh Token expired messages, so getting separate app passwords didn't help. Do you have any ideas how I could debug this further?

@MarshalX
Copy link
Owner

@Ph-St welp, first of all, as @emilyhunt asked, it would be great to see the code. are you sure that this is the exact copy of an example from the repo?

for debug purposes highly recommend starting with debug lvl logging from httpx (lib to perform requests). it will show how and when the session is created/refreshed etc

@Ph-St
Copy link
Author

Ph-St commented Jan 13, 2025

Ok sure, so here are the relevant parts of the code:

def get_session() -> Optional[str]:
    try:
        with open('session_requests_bs.txt', encoding='UTF-8') as f:
            return f.read()
    except FileNotFoundError:
        return None

def save_session(session_string: str) -> None:
    with open('session_requests_bs.txt', 'w', encoding='UTF-8') as f:
        f.write(session_string)

def on_session_change(event: SessionEvent, session: Session) -> None:
    print('Session changed:', event, repr(session))
    if event in (SessionEvent.CREATE, SessionEvent.REFRESH):
        print('Saving changed session')
        save_session(session.export())

def init_client() -> Client:
    client = Client()
    client.on_session_change(on_session_change)

    session_string = get_session()
    if session_string:
        print('Reusing session')
        client.login(session_string=session_string)
    else:
        # Login
        username="xxxxxxxx"
        password="xxxxxxxx"
        print('Creating new session')
        client.login(username, password)

    return client

async def check_messages_periodically():
    while True:
        await check_for_new_messages()
        await asyncio.sleep(2)

async def check_for_new_messages():
    # Initialize chat
    dm_client = client.with_bsky_chat_proxy()
    # create shortcut to convo methods
    dm = dm_client.chat.bsky.convo
    
    # Collect conversations with unread messages
    unread_convos = []
    convo_list = dm.list_convos()
    for convo in convo_list.convos:
        if convo.unread_count > 0:
            unread_convos.append(convo)

    if len(unread_convos) > 0:
        print(f'There are {(len(unread_convos))} conversation with unread messages.')

    for counter,ur_convo in enumerate(unread_convos, start=1):
        # Getting data from last messages
        print(f'Conversation {counter} ID: {ur_convo.id}')      
        messages = dm.get_messages(models.ChatBskyConvoGetMessages.Params(convoId=ur_convo.id))
        last_message_sender_did=messages['messages'][0]['sender']['did']
        last_message_text=messages['messages'][0]['text']
        
        last_message_id=messages['messages'][0]['id']
        print(f'Last sender DID: {last_message_sender_did}, last text: {last_message_text}, last id: {last_message_id}')

# Creating reply to message, no API calls here
# ...

# Send reply
        convo = dm.get_convo_for_members(models.ChatBskyConvoGetConvoForMembers.Params(members=[notification_account]),).convo
        dm.send_message(models.ChatBskyConvoSendMessage.Data(convo_id=convo.id,message=models.ChatBskyConvoDefs.MessageInput(text=reply,),))

        # Setting message status to read
        response = dm.update_read({"convoId": ur_convo.id,"messageId": last_message_id})

if __name__ == '__main__':
    client = init_client()
    asyncio.run(check_messages_periodically())

Can you spot anything problematic?

@MarshalX
Copy link
Owner

MarshalX commented Jan 13, 2025

oh i spot it. i think that's because of #371 bug :(

lemme try to describe what happens:

  • client manages the session, but performs requests only once (on login request).
  • the session refreshing happens right before a new request. so the client does not tries to refresh the session at all
  • dm_client created as a copy of client. And you perform requests using that copied client to get convos
  • dm_client refreshes the session right before the request when it should expired and the request passes
  • the next check_for_new_messages call will fail because the refreshed session was saved only in the previous copy of dm_client!

tldr; when you perform requests using dm_client it refreshes the session, with the original client not receiving refreshed tokens and still using expired ones!

upd. as a hacky and fast solution i suggest performing any requests with client right before calling with_bsky_chat_proxy. So the code will look like this:

# send any request to trigger the SDK session refreshing mechanism 
client.get_timeline()  # we do not care about the returned value

# Initialize chat with refreshed tokens
dm_client = client.with_bsky_chat_proxy()
# create shortcut to convo methods
dm = dm_client.chat.bsky.convo

@Ph-St
Copy link
Author

Ph-St commented Jan 13, 2025

@MarshalX I see! I added your fix. Let's see whether that solves it. Many thanks for the help!

@Ph-St
Copy link
Author

Ph-St commented Jan 15, 2025

@MarshalX Can't see any more errors in the log. Thanks again!

@Ph-St Ph-St closed this as completed Jan 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants