feat: add variable interpolation support for WebSocket requests (#6064)

* feat: add variable interpolation support for WebSocket requests

- Add WebSocket body interpolation in interpolateVars function
- Interpolate URL, headers, and all messages in request.body.ws array with full variable context
- Refactor sendWsRequest to use main process preparation (removes duplication)
- Add mode property to wsRequest object for proper request type detection
- Ensure consistent variable precedence matching HTTP/gRPC requests
- Centralize all interpolation logic in main process via prepareWsRequest

* Add Playwright tests for WebSocket variable interpolation

- Add tests for URL interpolation (wss://echo.{{url}}.org)
- Add tests for message content interpolation ({"test": "{{data}}"})
- Update test fixtures to use wss://echo.websocket.org echo server
- Add WEBSOCKET_FLOWS.md documentation
- Refactor queueWsMessage to handle variable interpolation in main process

* removed ws flow documentation

* chore: updated the network/index.js file to reduce merge conflicts by moving around code

* fix: added collection and item to WsQueryUrl Editor to fix available variable highlight

* chore: remove unnecessary whitespace in WebSocket event handlers

---------

Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
Chirag Chandrashekhar
2025-11-17 12:02:25 +05:30
committed by GitHub
parent 2d2a17c90f
commit 8ec1925b9f
9 changed files with 255 additions and 42 deletions

View File

@@ -81,6 +81,26 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
});
request.body = JSON.parse(parsed);
}
// Interpolate WebSocket message body
const isWsRequest = request.mode === 'ws';
if (isWsRequest && request.body && request.body.ws && Array.isArray(request.body.ws)) {
request.body.ws.forEach((message) => {
if (message && message.content) {
// Try to detect if content is JSON for proper escaping
let isJson = false;
try {
JSON.parse(message.content);
isJson = true;
} catch (e) {
// Not JSON, treat as regular string
}
message.content = _interpolate(message.content, {
escapeJSONStrings: isJson
});
}
});
}
if (typeof contentType === 'string') {
/*

View File

@@ -38,6 +38,8 @@ const prepareWsRequest = async (item, collection, environment, runtimeVariables,
mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath);
mergeAuth(collection, request, requestTreePath);
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
}
each(get(collectionRoot, 'request.headers', []), (h) => {
@@ -65,6 +67,7 @@ const prepareWsRequest = async (item, collection, environment, runtimeVariables,
let wsRequest = {
uid: item.uid,
mode: request.body.mode,
url: request.url,
headers,
processEnvVars,
@@ -276,15 +279,43 @@ const registerWsEventHandlers = (window) => {
}
});
ipcMain.handle('renderer:ws:queue-message', (event, requestId, collectionUid, message) => {
try {
wsClient.queueMessage(requestId, collectionUid, message);
return { success: true };
} catch (error) {
console.error('Error queuing WebSocket message:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('renderer:ws:queue-message',
async (event, { item, collection, environment, runtimeVariables, messageContent }) => {
try {
const itemCopy = cloneDeep(item);
const preparedRequest = await prepareWsRequest(itemCopy, collection, environment, runtimeVariables, {});
// If messageContent is provided, find and queue that specific message (interpolated)
// Otherwise, queue all messages
if (messageContent !== undefined && messageContent !== null) {
// Find the message index in the original request
const originalMessages = itemCopy.draft?.request?.body?.ws || itemCopy.request?.body?.ws || [];
const messageIndex = originalMessages.findIndex((msg) => msg.content === messageContent);
if (messageIndex >= 0 && preparedRequest.body?.ws?.[messageIndex]) {
// Queue the interpolated version of the specific message
wsClient.queueMessage(preparedRequest.uid, collection.uid, preparedRequest.body.ws[messageIndex].content);
} else {
// Message not found in request body, queue as-is (shouldn't happen in normal flow)
wsClient.queueMessage(preparedRequest.uid, collection.uid, messageContent);
}
} else {
// Queue all messages (they are already interpolated by prepareWsRequest -> interpolateVars)
if (preparedRequest.body && preparedRequest.body.ws && Array.isArray(preparedRequest.body.ws)) {
preparedRequest.body.ws
.filter((message) => message && message.content)
.forEach((message) => {
wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content);
});
}
}
return { success: true };
} catch (error) {
console.error('Error queuing WebSocket message:', error);
return { success: false, error: error.message };
}
});
// Send a message to an existing WebSocket connection
ipcMain.handle('renderer:ws:send-message', (event, requestId, collectionUid, message) => {