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

fix: pass headers when connecting sockets #1575

Merged
merged 10 commits into from
Dec 12, 2024
Merged

Conversation

willydouhard
Copy link
Collaborator

@willydouhard willydouhard commented Dec 9, 2024

Fixing #1279 and closes #1359 as well. Tested it on GCP with session affinity + 2 instances with this docker file

# Build stage for Python application
FROM python:3.11-slim

# Install system dependencies
RUN apt-get update && apt-get install -y \
    git \
    nodejs \
    npm \
    && rm -rf /var/lib/apt/lists/*

# Install pnpm
RUN npm install -g pnpm

# Set working directory
WORKDIR /app

# Copy your application files
COPY app.py .

# Install Python dependencies
RUN pip install --no-cache-dir git+https://github.com/Chainlit/chainlit.git@willy/sticky-sessions#subdirectory=backend/

# Set environment variables
ENV PORT=8000
ENV HOST=0.0.0.0

# Expose the port
EXPOSE 8000

# Start the application
CMD ["chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "8000", "-h"]

@dosubot dosubot bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Dec 9, 2024
@willydouhard
Copy link
Collaborator Author

This is failing for any origin that is the not the server's origin (typically Chainlit embedded as a copilot on a website) because withCredential does not support '*' cross origin.

Access to XMLHttpRequest at 'https://chainlit-sticky-182925357376.europe-west2.run.app/ws/socket.io/?EIO=4&transport=polling&t=PEh9E88' from origin 'http://localhost:9000/' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

@dosubot dosubot bot added size:S This PR changes 10-29 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels Dec 9, 2024
@willydouhard
Copy link
Collaborator Author

The issue is fixed. For cross origin use cases, it is now mandatory to allow it in the project config (I had to update the copilot test for example) as the wildcard "*" is not supported with credentials.

@@ -100,6 +100,8 @@ const useChatSession = () => {

const socket = io(uri, {
path,
withCredentials: true,
transports: ['websocket'],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change works for our use-case, but I believe adding the transports option here means that Socket.IO will not fall back to long-polling if a WS connection can't be established. There are probably still some load balancers/browsers out there that can't handle web-sockets properly. It might be helpful for these options to become chainlit initialization options that the user can select?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance the gcp load balancer only works with this. We can make it configurable I think.

@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Dec 9, 2024
}
withCredentials: true,
transports: ['websocket'],
auth: {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to move out from the extra header approach since the websocket transport does not support that.

@@ -311,6 +311,8 @@ class CodeSettings:
@dataclass()
class ProjectSettings(DataClassJsonMixin):
allow_origins: List[str] = Field(default_factory=lambda: ["*"])
# Socket.io client transports option
transports: Optional[List[str]] = None
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made the transports configurable @evantahler

@willydouhard
Copy link
Collaborator Author

Here is the doc PR Chainlit/docs#199

@evantahler
Copy link

I gave this branch a try within our application, but every request I made is rejected:

  • Clint WS message: {"message":"Connection rejected by server"}
  • Server: {"type":"LOG","log":{"level":"INFO","message":"Authentication failed"}}

We have a custom data layer, but I tried removing it with the same (failing) results. Any tips?

@evantahler
Copy link

Note: Poetry install line for pyproject.toml

chainlit = { git = "ssh://[email protected]/chainlit/chainlit.git", branch = "willy/sticky-sessions", subdirectory = "backend" }

@dokterbob
Copy link
Collaborator

Note: Poetry install line for pyproject.toml

chainlit = { git = "ssh://[email protected]/chainlit/chainlit.git", branch = "willy/sticky-sessions", subdirectory = "backend" }

Convenient one-liner - not requiring authentication/write access to the repo:

poetry add git+https://github.com/Chainlit/chainlit.git@willy/sticky-sessions#subdirectory=backend/

@dokterbob
Copy link
Collaborator

I gave this branch a try within our application, but every request I made is rejected:

  • Clint WS message: {"message":"Connection rejected by server"}
  • Server: {"type":"LOG","log":{"level":"INFO","message":"Authentication failed"}}

We have a custom data layer, but I tried removing it with the same (failing) results. Any tips?

Could you share a screendump of the browser's console and network requests from the debugger? Particularly the websocket initialisation process. Details on the first non-successful request from the network debugger would also help.

@dokterbob
Copy link
Collaborator

@dvdr00t This probably also solves #1407. Any chance you could give it a try? More eyes on this the better. :)

@evantahler
Copy link

Sure! We are using a FastAPI application & mount_chainlit

Server Logs:

10:00:54 api.1       | INFO:     127.0.0.1:57431 - "GET /api/v1/users/me HTTP/1.1" 200 OK
10:00:54 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:54 api.1       | {"type":"LOG","log":{"level":"WARN","message":"Translated markdown file for en-US not found. Defaulting to chainlit.md."}}
10:00:54 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:54 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/project/settings?language=en-US HTTP/1.1" 200 OK
10:00:54 api.1       | INFO:     127.0.0.1:57434 - "GET /api/v1/chat/auth HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /api/v1/chat/auth HTTP/1.1" 200 OK
10:00:55 api.1       | {"type":"LOG","log":{"level":"WARN","message":"Translated markdown file for en-US not found. Defaulting to chainlit.md."}}
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/project/settings?language=en-US HTTP/1.1" 200 OK
10:00:55 api.1       | {"type":"LOG","log":{"level":"WARN","message":"Translated markdown file for en-US not found. Defaulting to chainlit.md."}}
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/project/settings?language=en-US HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "POST /chat/project/threads HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "POST /chat/project/threads HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6pmpfx HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6pmpfx HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6qz0a3&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     ('127.0.0.1', 57441) - "WebSocket /chat/ws/socket.io/?EIO=4&transport=websocket&sid=ochdon-Bc-f-hV5YAAAA" [accepted]
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6r0pdd&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     connection open
10:00:55 api.1       | {"type":"LOG","log":{"level":"INFO","message":"Authentication failed"}}
10:00:55 api.1       | INFO:     127.0.0.1:57443 - "POST /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6qz0a3&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6r0pdd&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6td00j&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57443 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6td00j&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57443 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6v2btm&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6v2btm&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     connection closed

Some screenshots from the browser console:

Screenshot 2024-12-10 at 9 59 58 AM Screenshot 2024-12-10 at 9 59 32 AM

@dokterbob
Copy link
Collaborator

Incidentally, a prior PR for this issue was already open #1339. Should close it once we merge this.

@dokterbob
Copy link
Collaborator

Sure! We are using a FastAPI application & mount_chainlit

Server Logs:

10:00:54 api.1       | INFO:     127.0.0.1:57431 - "GET /api/v1/users/me HTTP/1.1" 200 OK
10:00:54 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:54 api.1       | {"type":"LOG","log":{"level":"WARN","message":"Translated markdown file for en-US not found. Defaulting to chainlit.md."}}
10:00:54 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:54 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/project/settings?language=en-US HTTP/1.1" 200 OK
10:00:54 api.1       | INFO:     127.0.0.1:57434 - "GET /api/v1/chat/auth HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /api/v1/chat/auth HTTP/1.1" 200 OK
10:00:55 api.1       | {"type":"LOG","log":{"level":"WARN","message":"Translated markdown file for en-US not found. Defaulting to chainlit.md."}}
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/project/settings?language=en-US HTTP/1.1" 200 OK
10:00:55 api.1       | {"type":"LOG","log":{"level":"WARN","message":"Translated markdown file for en-US not found. Defaulting to chainlit.md."}}
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/auth/config HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "GET /chat/project/settings?language=en-US HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "POST /chat/project/threads HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "POST /chat/project/threads HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6pmpfx HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6pmpfx HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6qz0a3&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     ('127.0.0.1', 57441) - "WebSocket /chat/ws/socket.io/?EIO=4&transport=websocket&sid=ochdon-Bc-f-hV5YAAAA" [accepted]
10:00:55 api.1       | INFO:     127.0.0.1:57431 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6r0pdd&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     connection open
10:00:55 api.1       | {"type":"LOG","log":{"level":"INFO","message":"Authentication failed"}}
10:00:55 api.1       | INFO:     127.0.0.1:57443 - "POST /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6qz0a3&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6r0pdd&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6td00j&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57443 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6td00j&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57443 - "OPTIONS /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6v2btm&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     127.0.0.1:57434 - "GET /chat/ws/socket.io/?EIO=4&transport=polling&t=rp6v2btm&sid=ochdon-Bc-f-hV5YAAAA HTTP/1.1" 200 OK
10:00:55 api.1       | INFO:     connection closed

Some screenshots from the browser console:

Screenshot 2024-12-10 at 9 59 58 AM Screenshot 2024-12-10 at 9 59 32 AM

Thanks. That was quick! Could you send the browser's Console as well?

I am about to test with a similar setup for our own application RN.

@dokterbob
Copy link
Collaborator

dokterbob commented Dec 10, 2024

@evantahler The auth error comes from here:

logger.info("Authentication failed")

Could you remove the try/except there, or change it into logger.exception() to get the underlying Exception?

@dokterbob
Copy link
Collaborator

dokterbob commented Dec 10, 2024

On my setup (no custom data layer, but using mount_chainlit(), with a single instance, I don't get authorization errors.

With two instances:

{"detail":"Unauthorized"}

This happens with OAuth authorisation, after logging out, when attempting to log back in. The request is handled by the same instance:
image

This seems a different error from the one you've been getting:

    # Check the state from the oauth provider against the browser cookie
    oauth_state = request.cookies.get("oauth_state")
    if oauth_state != state:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Unauthorized",
        )

@evantahler
Copy link

The browser console for this page is just tons of react errors about tooltips and mimetypes. Here's the output in a gist - https://gist.github.com/evantahler/171f6aa0a9f862c0122ec0728d8c430c

@dokterbob
Copy link
Collaborator

The browser console for this page is just tons of react errors about tooltips and mimetypes.

🤣

@dokterbob
Copy link
Collaborator

GTG now, happy to look again tomorrow. Any discriminating factors (what auth are you using) and/or feedback from other users greatly appreciated.

@evantahler
Copy link

evantahler commented Dec 10, 2024

@evantahler The auth error comes from here:

logger.info("Authentication failed")

Could you remove the try/except there, or change it into logger.exception() to get the underlying Exception?

Looks like my auth is none:

10:45:00 api.1       | {"type":"LOG","log":{"level":"ERROR","message":"Authentication failed\nTraceback (most recent call last):\n
  File \"/Users/evan/Library/Caches/pypoetry/virtualenvs/app---U6Ca7R-py3.10/lib/python3.10/site-packages/socketio/async_server.py\", line 555, in _handle_connect\n
    success = await self._trigger_event(\n
  File \"/Users/evan/Library/Caches/pypoetry/virtualenvs/app---U6Ca7R-py3.10/lib/python3.10/site-packages/socketio/async_server.py\", line 637, in _trigger_event\n
    ret = await handler(*args)\n
TypeError: connect() missing 1 required positional argument: 'auth'\n\n
During handling of the above exception, another exception occurred:\n\n
Traceback (most recent call last):\n
  File \"/Users/evan/Library/Caches/pypoetry/virtualenvs/app---U6Ca7R-py3.10/lib/python3.10/site-packages/chainlit/socket.py\", line 97, in connect\n
    token = auth.get(\"token\")\n
AttributeError: 'NoneType' object has no attribute 'get'"}}

(I added some line breaks)

@evantahler
Copy link

evantahler commented Dec 10, 2024

Perhaps I'm still using an older version of the UI... any way to confirm that it has been re-compiled/re-built?

Edit: Screenshot 2024-12-10 at 11 01 15 AM

I /think/ that the fact that the websocket request is not including any cookie header matches this

@dokterbob
Copy link
Collaborator

Perhaps I'm still using an older version of the UI... any way to confirm that it has been re-compiled/re-built?

Edit: Screenshot 2024-12-10 at 11 01 15 AM

I /think/ that the fact that the websocket request is not including any cookie header matches this

Yes, that seems most likely. Can you share your Dockerfile?

Normally, poetry install should trigger npm buildUi, which builds the UI - then it copies it to chainlit/backend/chainlit/frontend/dist` from where chainlit serves it.

However, if you are running a chainlit install from the repo (as should be the case for source builds), AFAIK it serves from frontend/dist instead.

The obvious way is to add a console.log() somewhere. ;)

@willydouhard
Copy link
Collaborator Author

Most likely using an old version of the UI. The auth change happens in the react-client (which is supposed to be rebuilt as well by pnpm run buildUi).

Copy link
Collaborator

@dokterbob dokterbob left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just done repeated login/logout testing with 4 instances on GCP with OAuth. Used private browser and normal browser. No problems whatsoever. I think we should merge this and have users test it in a release candidate.!

@@ -13,7 +13,7 @@ session_timeout = 3600
cache = false

# Authorized origins
allow_origins = ["*"]
allow_origins = ["http://127.0.0.1:8000"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love finally seeing this! :)

We should document that on a server users need to configure the origin.

except StopIteration:
return str(uuid.uuid5(uuid.NAMESPACE_DNS, ip))


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to see this go! :)

@@ -149,7 +132,7 @@ def emit_call_fn(event: Literal["ask", "call_fn"], data, timeout):
user=user,
token=token,
chat_profile=chat_profile,
thread_id=environ.get("HTTP_X_CHAINLIT_THREAD_ID"),
thread_id=auth.get("threadId"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems much cleaner here.

@evantahler
Copy link

I think we should merge this and have users test it in a release candidate!

Excellent! Can you comment here when that RC is ready to try out?

@asvishnyakov
Copy link

asvishnyakov commented Dec 19, 2024

@willydouhard @dokterbob Just installed chainlit from main branch and got the same message about TypeError: connect() missing 1 required positional argument: 'auth'\n\n

UPD. I confirm downgrading to previous commit resolves the issue. poetry updates frontend in backend folder fine, I checked index file hash. Please investigate further.

UPD2. It looks like pip isn't propely downgrade packages, so if you install chainlit via git, you want to fully recreate environment in order to downgrade and fix an issue.

@willydouhard
Copy link
Collaborator Author

I am not able to reproduce, did you rebuild the entire UI including the react client? The auth change is actually in the react client which is a dependency of the main frontend.

@willydouhard
Copy link
Collaborator Author

The pre-release is available @evantahler @asvishnyakov.

pip3 install -U --pre chainlit

Let me know if it works on your end.

@asvishnyakov
Copy link

asvishnyakov commented Dec 19, 2024

@willydouhard Thank you! This resolved the issue. Glad to be able use new version, it has amazing features!

@evantahler
Copy link

We are using a custom frontend (@chainlit/react-client), and it looks like a new version of the frontend hasn't been pushed to NPM yet https://www.npmjs.com/package/@chainlit/react-client?activeTab=versions

@willydouhard
Copy link
Collaborator Author

I just released it, version 0.2.1.

We are using a custom frontend (@chainlit/react-client), and it looks like a new version of the frontend hasn't been pushed to NPM yet https://www.npmjs.com/package/@chainlit/react-client?activeTab=versions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:M This PR changes 30-99 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Security: allowed origins should not be * by default
4 participants