Unable to connect using websocket with forwarded port by an app in the workspace

Hi, I have an application that run an HTTP server with websocket connexions. When I launch the app in the workspace Coder create forwarded port as expected but when I try to connect to the application the ws connexions are refused (see image). I tried the same thing with codespaces (to test if it was my app or Coder) and it works as expected. Do you know if I had to do a special configuration to enable the websocket connection from a forwarded port? Thanks
No description
26 Replies
Phorcys
Phorcys2w ago
@Stéphane is it possible to get the detail of that specific request that fails? also, are you running any reverse proxy or load balancer in front of Coder that might be causing this somehow?
scraly
scraly2w ago
Coder is deployed on a kubernetes cluster with Helm (ingress nginx).
Stéphane
StéphaneOP2w ago
Here the HTML page that use websocket to do the connexion, the websocket part begining at the line 2023:
// Connection management functions
function createWebSocketConnection() {
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// contains hostname + port
const host = window.location.host;
ws = new WebSocket(`${protocol}//${host}/_/ws`);

ws.onclose = () => {
console.log('WebSocket closed');
handleConnectionLoss();
};

ws.onmessage = handleWebSocketMessage;

ws.onopen = () => {
window.javelit.sendPathUpdate();
connectionState = 'connected';
console.log('WebSocket connected');
connectionModal.show = false;
otherErrorModal.show = false;
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

} catch (error) {
console.error('Failed to create WebSocket:', error);
handleConnectionLoss();
}
}

function handleConnectionLoss() {
if (connectionState === 'connected') {
// First time losing connection - show the connection lost modal
// wait 1s to avoid modal flickerings when a page is reloaded
setTimeout( () => {connectionModal.show = true;}, 1000)
}
connectionState = 'reconnecting';
setTimeout(() => {
console.log('Attempting to reconnect...');
createWebSocketConnection();
}, 5000);

// Clear websocket references
ws = null;
}
// Connection management functions
function createWebSocketConnection() {
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// contains hostname + port
const host = window.location.host;
ws = new WebSocket(`${protocol}//${host}/_/ws`);

ws.onclose = () => {
console.log('WebSocket closed');
handleConnectionLoss();
};

ws.onmessage = handleWebSocketMessage;

ws.onopen = () => {
window.javelit.sendPathUpdate();
connectionState = 'connected';
console.log('WebSocket connected');
connectionModal.show = false;
otherErrorModal.show = false;
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

} catch (error) {
console.error('Failed to create WebSocket:', error);
handleConnectionLoss();
}
}

function handleConnectionLoss() {
if (connectionState === 'connected') {
// First time losing connection - show the connection lost modal
// wait 1s to avoid modal flickerings when a page is reloaded
setTimeout( () => {connectionModal.show = true;}, 1000)
}
connectionState = 'reconnecting';
setTimeout(() => {
console.log('Attempting to reconnect...');
createWebSocketConnection();
}, 5000);

// Clear websocket references
ws = null;
}
Stéphane
StéphaneOP2w ago
and the original source file (the one attched is from my browser "view source" ;)): https://github.com/javelit/javelit/blob/0c6e9b7f08b88fa2de6dfcd01b9ac50512c72b90/src/main/resources/index.html.mustache#L641
GitHub
javelit/src/main/resources/index.html.mustache at 0c6e9b7f08b88fa2d...
The simplest way to build data apps and webapps in Java. Inspired by Streamlit. - javelit/javelit
Phorcys
Phorcys2w ago
oh okay, @Stéphane I get it, the code assumes that the app is runnign on the root of the domain, rather than being on a sub-path, so the request makes it to Coder and Coder refuses that connection because it's not aware of that endpoint are you able to issue a wildcard certificate on the Coder subdomain at all? e.g *.workshop.labdevrel.ovh
scraly
scraly2w ago
Thanks for the answer the fact is that we are using cert-manager to generate the SSl certificate and we are using HTTP01 for the cert challenge But the problem is: "Wildcard certificates are not supported with HTTP01 validation and require DNS01." So i'm checking if we can use DNS01 in our infra 😅
Stéphane
StéphaneOP2w ago
@Phorcys where (in the code) did you see that the ws connexion uses a subdomain? In the code I see this: ws = new WebSocket(${protocol}//${host}/_/ws); but no use of subdomain, right? The forwarded port is: https://workshop.labdevrel.ovh/%40stephane/sopra-spt-01.main/apps/code-server/proxy/8080/ so for us only the domain workshop.labdevrel.ovh is used by the previous code line. One difference with Code Spaces URL is that the Code Spaces URL has no sub path (https://stunning-space-bassoon-5v5gwg6rjp6f7rjj-8080.app.github.dev/), I don't know if it's important.
Phorcys
Phorcys2w ago
@Stéphane basically, the issue is that your app is hosted at https://workshop.labdevrel.ovh/%40stephane/sopra-spt-01.main/apps/code-server/proxy/8080 but the program logic (new WebSocket(`${protocol}//${host}/_/ws`);) here will cause the websocket url to be: wss://workshop.labdevrel.ovh/_/ws instead of wss://workshop.labdevrel.ovh/%40stephane/sopra-spt-01.main/apps/code-server/proxy/8080/_/ws because it assumes that the main app is being served on / it is, that's the root cause of your issue
Phorcys
Phorcys2w ago
also, for what it's worth, i would recommend not using code-server for the proxying but using Coder's built-in port forward
No description
Phorcys
Phorcys2w ago
or even better, define a coder_app resource in the template! what you need to do is either provision a wildcard *.workshop.labdevrel.ovh certificate to allow Coder to do the same as CodeSpaces in this scenario OR modify the app to take into account the different path that the app is being served at
Stéphane
StéphaneOP2w ago
from which version I have the option Listening Ports ? I have not it the menu in my version (v2.25.1+3bf6a00)
No description
Phorcys
Phorcys2w ago
you will get it automatically if you enable the wildcard subdomain for Coder you can also do CODER_DANGEROUS_ALLOW_PATH_APP_SHARING=true if you can't do wildcard it is unsafe, but technically not more unsafe than what you're already doing through code-server path-based sharing is not recommended because it causes the apps to be able to access the Coder authentication cookies, which is why i'm recommending wildcard instead :)
Stéphane
StéphaneOP2w ago
If I well understand the coder_app resource the url attribute must be an available URL but in my case I launch manually the undertow server so at the workspace starting the webserver is not running. It's just launched sometimes to test some part of code.
Phorcys
Phorcys2w ago
you can do that still, it's just that the url will only work when the app is running, it's just more friendly because then you'd have a button for your testing app you can also set a healthcheck so that the button is greyed out when it's not running
Stéphane
StéphaneOP2w ago
I've still the error with this coder_app declaration:
resource "coder_app" "javelit" {
agent_id = coder_agent.main.id
slug = "javelit"
icon = "https://avatars.githubusercontent.com/u/233158489?s=200&v=4"
url = "http://localhost:8080"
subdomain = false
share = "authenticated"
healthcheck {
url = "http://localhost:8080"
interval = 10
threshold = 30
}
}
resource "coder_app" "javelit" {
agent_id = coder_agent.main.id
slug = "javelit"
icon = "https://avatars.githubusercontent.com/u/233158489?s=200&v=4"
url = "http://localhost:8080"
subdomain = false
share = "authenticated"
healthcheck {
url = "http://localhost:8080"
interval = 10
threshold = 30
}
}
Is it because I set the subdomain to false ?
Phorcys
Phorcys2w ago
what error? oh, the websocket, yeah that's won't fix the issue that is in the app just recommended to do that instead of code-server proxy
cyrilou242
cyrilou2422w ago
Hey I'm the maintainer of Javelit, thanks for looking at this, I am working on a fix. Is the proxy passing a header with the url subpath ? For instance something like X-Forwarded-Prefix ? (there is no standard for this but it's a common value) Eg: X-Forwarded-Prefix: /sopra-spt-01.main/apps/code-server/proxy/8080
Phorcys
Phorcys7d ago
hey! thanks for joining, it is not passed AFAIK but it can be if absolutely necessary usually we expect apps to assume that the websocket path should be relative to the path where the app is served at but, in the past i've seen it done through a query parameter e.g a ?path=/base/path that is computed by the template -> https://github.com/uwu/basic-env/blob/59d7e8d9b0328ffa59654eb8de79f200a930fcd6/main.tf#L256
cyrilou242
cyrilou2427d ago
thanks, the issue is also impacting other places in the codebase related to page routing and media serving. - javelit supports pages so url may look like {coder.com_proxy_sub_path}/{javelit_sub_path} - media urls should have the subpath as prefix (I can't use relative as the user may be in a javelit subpath) I don't think there is a way to properly infer the subpath in all cases (Streamlit and Gradio were not able to do it either) so I need the subpath explicitely. In terms of solution: 1. I'm not a fan of the url parameter because if it's not passed or lost then the app is broken, and it'll be confusing. 2. I think the X-Forwarded-Prefix is a better solution, as it means javelit will behave properly automatically, can be served by multiple proxies on different sub_paths transparently, etc... 3. in the meantime, similar to the url parameter, I'll implement passing the subpath manually via command line / server argument when launching javelit eg with something like
javelit run ... --basePathUrl=/sopra-spt-01.main/apps/code-server/proxy/8080
javelit run ... --basePathUrl=/sopra-spt-01.main/apps/code-server/proxy/8080
if I understand the code you shared correctly in the coder config it will look like:
javelit run ... --basePathUrl=/${data.coder_workspace.some.variable.name}/apps/code-server/proxy/8080
javelit run ... --basePathUrl=/${data.coder_workspace.some.variable.name}/apps/code-server/proxy/8080
(the 8080 port can also be templatized I guess)
Phorcys
Phorcys7d ago
yeah, that base path approach is also used by some other apps X-Forwarded-Path, sure, but that needs a trust mechanism to only allow it from specific hosts
Stéphane
StéphaneOP7d ago
@cyrilou242 if you need a Coder instance with a fresh workspace to do some tests, I can give you an access to my platform 😉
cyrilou242
cyrilou2427d ago
I'm good, writing my own proxy in the e2e test 🙂 the fix should be ready in an hour Hey @Stéphane you can pass Javelit the subpath it's running on with:
--base-path=/sopra-spt-01.main/apps/code-server/proxy/8080
--base-path=/sopra-spt-01.main/apps/code-server/proxy/8080
(I'll let you replace with coder.com variables where relevant ${data.coder_workspace.some.variable.name} This is available in Javelit version 0.73.0 --base-path CLI doc:
URL path prefix where the Javelit app is served. By default, Javelit expects to be served at the root "/".
For instance, if Javelit is served behind a proxy at example.com/behind/proxy, use "--base-path=/behind/proxy".
This setting is not necessary if the proxy sets the X-Forwarded-Prefix header.
When using --base-path, if you need to access the app on localhost directly (not behind the proxy), pass the ?ignoreBasePath=true query parameter. Eg: localhost:8080/?ignoreBasePath=true
In dev mode, the browser automatically opens with ?ignoreBasePath=true.
URL path prefix where the Javelit app is served. By default, Javelit expects to be served at the root "/".
For instance, if Javelit is served behind a proxy at example.com/behind/proxy, use "--base-path=/behind/proxy".
This setting is not necessary if the proxy sets the X-Forwarded-Prefix header.
When using --base-path, if you need to access the app on localhost directly (not behind the proxy), pass the ?ignoreBasePath=true query parameter. Eg: localhost:8080/?ignoreBasePath=true
In dev mode, the browser automatically opens with ?ignoreBasePath=true.
Stéphane
StéphaneOP7d ago
I will test ASAP 😉 Big thanks 🫶
Phorcys
Phorcys7d ago
nice! @cyrilou242 amazing, thanks for the work
Stéphane
StéphaneOP7d ago
Everything works as expected on Coder now! Thanks a lot, you save my back!
scraly
scraly6d ago
Thanks! 🥰

Did you find this page helpful?