Error while loading document in Collabora: Invalid URI and WebSocket closed errors

Title: “Error while loading document in Collabora: Invalid URI and WebSocket closed errors”

Hi everyone,

I’m trying to integrate Collabora Online (CODE) with a custom backend that implements WOPI APIs.


I’m running into some issues while trying to load documents inside the iframe.

Here are the important parts of my backend implementation:

// GET file metadata
collaboraRouter.get('/files/:fileId', async (req, res) => {
  try {
    const { fileId } = req.params;
    const { access_token } = req.query;

    if (!access_token || !ACCESS_TOKENS.has(access_token)) {
      return res.status(401).json({ error: 'Invalid access token' });
    }

    const tokenData = ACCESS_TOKENS.get(access_token);
    if (tokenData.fileId !== fileId) {
      return res.status(403).json({ error: 'Token not valid for this file' });
    }

    const file = bucket.file(`submissions/${fileId}`);
    const [metadata] = await file.getMetadata();

    res.json({
      BaseFileName: fileId,
      Size: parseInt(metadata.size),
      OwnerId: "user1",
      UserId: "user1",
      Version: metadata.generation || '1',
      LastModifiedTime: metadata.updated,
      UserFriendlyName: "User",
      UserCanWrite: true,
      DisablePrint: false,
      DisableExport: false,
      DisableCopy: false,
      HideSaveOption: false,
      PostMessageOrigin: "http://localhost:5173"
    });
  } catch (error) {
    console.error('[WOPI] Error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// GET file contents
collaboraRouter.get('/files/:fileId/contents', async (req, res) => {
  try {
    const { fileId } = req.params;
    const { access_token } = req.query;

    if (!access_token || !ACCESS_TOKENS.has(access_token)) {
      return res.status(401).json({ error: 'Invalid access token' });
    }

    const file = bucket.file(`submissions/${fileId}`);
    const stream = file.createReadStream();
    stream.pipe(res);
  } catch (error) {
    console.error('[WOPI] Error streaming file:', error);
    res.status(500).json({ error: 'Failed to stream file' });
  }
});

// GET discovery XML
collaboraRouter.get('/hosting/discovery', async (req, res) => {
  try {
    console.log('Fetching discovery.xml from Collabora');
    const response = await axios.get(`${COLLABORA_HOST}/hosting/discovery`);
    res.set('Content-Type', 'application/xml');
    res.send(response.data);
  } catch (error) {
    console.error('Error getting discovery XML:', error);
    res.status(500).json({ error: 'Failed to get discovery XML' });
  }
});
// GET file metadata
collaboraRouter.get('/files/:fileId', async (req, res) => {
  try {
    const { fileId } = req.params;
    const { access_token } = req.query;

    if (!access_token || !ACCESS_TOKENS.has(access_token)) {
      return res.status(401).json({ error: 'Invalid access token' });
    }

    const tokenData = ACCESS_TOKENS.get(access_token);
    if (tokenData.fileId !== fileId) {
      return res.status(403).json({ error: 'Token not valid for this file' });
    }

    const file = bucket.file(`submissions/${fileId}`);
    const [metadata] = await file.getMetadata();

    res.json({
      BaseFileName: fileId,
      Size: parseInt(metadata.size),
      OwnerId: "user1",
      UserId: "user1",
      Version: metadata.generation || '1',
      LastModifiedTime: metadata.updated,
      UserFriendlyName: "User",
      UserCanWrite: true,
      DisablePrint: false,
      DisableExport: false,
      DisableCopy: false,
      HideSaveOption: false,
      PostMessageOrigin: "http://localhost:5173"
    });
  } catch (error) {
    console.error('[WOPI] Error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// GET file contents
collaboraRouter.get('/files/:fileId/contents', async (req, res) => {
  try {
    const { fileId } = req.params;
    const { access_token } = req.query;

    if (!access_token || !ACCESS_TOKENS.has(access_token)) {
      return res.status(401).json({ error: 'Invalid access token' });
    }

    const file = bucket.file(`submissions/${fileId}`);
    const stream = file.createReadStream();
    stream.pipe(res);
  } catch (error) {
    console.error('[WOPI] Error streaming file:', error);
    res.status(500).json({ error: 'Failed to stream file' });
  }
});

// GET discovery XML
collaboraRouter.get('/hosting/discovery', async (req, res) => {
  try {
    console.log('Fetching discovery.xml from Collabora');
    const response = await axios.get(`${COLLABORA_HOST}/hosting/discovery`);
    res.set('Content-Type', 'application/xml');
    res.send(response.data);
  } catch (error) {
    console.error('Error getting discovery XML:', error);
    res.status(500).json({ error: 'Failed to get discovery XML' });
  }
});

Frontend Console Logs:

Loading document with fileId: temp.docx
WOPI source URL: http://localhost:8080/api/wopi/files/temp.docx
Found action URL: http://localhost:9980/browser/.../cool.html?
Final iframe URL: http://localhost:9980/browser/.../cool.html?WOPISrc=http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fwopi%2Ffiles%2Ftemp.docx&access_token=******

When the iframe tries to load, I get WebSocket connection errors:

WebSocket connection to ‘ws://localhost:9980/cool/http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fwopi%2Ffiles%2Ftemp.docx%3Faccess_token%3D******_ttl%3D0%26permission%3Dedit/ws?WOPISrc=http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fwopi%2Ffiles%2Ftemp.docx&compat=/ws’ failed: WebSocket is closed before the connection is established.

Collabora Server (Docker) Logs:

wsd-00001-00031 2025-04-23 16:48:11.844812 +0000 [ websrv_poll ] ERR #42: Error while handling Client WS Request: Invalid URI.| wsd/ClientRequestDispatcher.cpp:2259
wsd-00001-00031 2025-04-23 16:48:12.205977 +0000 [ websrv_poll ] ERR #43: Error while handling Client WS Request: Invalid URI.| wsd/ClientRequestDispatcher.cpp:2259

Summary:

  • Discovery XML is fetched successfully.
  • File metadata and file contents API endpoints seem to respond properly when tested individually.
  • iframe URL is constructed and loaded, but WebSocket connections are failing.
  • Docker container logs show Invalid URI errors.
  • Testing locally on localhost, Collabora server is at localhost:9980, backend API is at localhost:8080.

Environment Details:

  • Backend: Node.js with Express
  • Collabora CODE: Docker image (latest)
  • Frontend: React
  • Running everything on local environment (localhost)

Hii @Sasidhar3159 Welcome to collabora online forums

In your iframe URL, you’re URL-encoding WOPISrc, but Collabora expects the WebSocket path to remain properly decoded inside the WebSocket upgrade request.

Specifically:
You’re passing:

WOPISrc=http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fwopi%2Ffiles%2Ftemp.docx

…but the WebSocket URL ends up like:

ws://localhost:9980/cool/http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fwopi%2Ffiles%2Ftemp.docx...

and Collabora’s WebSocket handler expects a path like:

ws://localhost:9980/cool/http://localhost:8080/api/wopi/files/temp.docx...

(not URL-encoded inside the /cool/... part).


Why?

  • When you access /browser/cool.html?..., you can URL-encode parameters.
  • But for WebSocket connections, the path (after /cool/) must be not encoded, or else Collabora’s ClientRequestDispatcher can’t parse it correctly.
  • If it sees %3A, %2F, etc., it says “Invalid URI” — exactly the error you see.

Make sure the iframe src is constructed correctly, so that the part after /cool/ is decoded.

The general flow is:

  1. Browser loads:
http://localhost:9980/browser/....cool.html?WOPISrc=encoded_url&access_token=xxx

Here, URL-encoding the WOPISrc is fine.

  1. Inside cool.html, when Collabora sets up the WebSocket, it tries to connect to:
ws://localhost:9980/cool/WOPISrc_path_and_query/ws

At this point, if WOPISrc was encoded in the wrong way, it breaks.


Thanks
Darshan