Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
import GrpcBody from 'components/RequestPane/GrpcBody';
import GrpcAuth from './GrpcAuth/index';
import GrpcAuthMode from './GrpcAuth/GrpcAuthMode/index';
import GrpcSettingsPane from 'components/RequestPane/GrpcSettingsPane';
import StatusDot from 'components/StatusDot/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import find from 'lodash/find';
Expand Down Expand Up @@ -41,6 +42,9 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
case 'auth': {
return <GrpcAuth item={item} collection={collection} />;
}
case 'settings': {
return <GrpcSettingsPane item={item} collection={collection} />;
}
case 'docs': {
return <Documentation item={item} collection={collection} />;
}
Expand Down Expand Up @@ -90,6 +94,11 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
label: 'Auth',
indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type="default" /> : null
},
{
key: 'settings',
label: 'Settings',
indicator: null
},
{
key: 'docs',
label: 'Docs',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import get from 'lodash/get';
import SettingsInput from 'components/SettingsInput';
import ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';
import { updateItemSettings } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';

const getPropertyFromDraftOrRequest = (propertyKey, item) =>
item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {});

const GrpcSettingsPane = ({ item, collection }) => {
const dispatch = useDispatch();

const settings = getPropertyFromDraftOrRequest('settings', item);
const {
maxReceiveMessageLength = '',
maxSendMessageLength = '',
deadline = '',
keepaliveTime = '',
keepaliveTimeout = '',
clientIdleTimeout = '',
maxReconnectBackoff = '',
includeDefaultValues
} = settings;

const updateSetting = useCallback((key, value) => {
dispatch(updateItemSettings({
collectionUid: collection.uid,
itemUid: item.uid,
settings: { [key]: value }
}));
}, [dispatch, collection.uid, item.uid]);

const onNumericChange = useCallback((key) => (e) => {
const value = e.target.value;
if (value === '' || value === '-1' || /^-?\d+$/.test(value)) {
updateSetting(key, value === '' ? '' : value);
}
}, [updateSetting]);

const onSave = useCallback(() => {
dispatch(saveRequest(item.uid, collection.uid));
}, [dispatch, item.uid, collection.uid]);

const handleKeyDown = useCallback((e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
onSave();
}
}, [onSave]);

return (
<div className="h-full w-full">
<div className="text-xs mb-4 text-muted">Configure gRPC channel and request settings.</div>
<div className="bruno-form">
<div className="flex flex-col gap-4">
<SettingsInput
id="maxReceiveMessageLength"
label="Max Receive Message Size"
value={maxReceiveMessageLength}
onChange={onNumericChange('maxReceiveMessageLength')}
description="Maximum response message size in bytes (-1 for unlimited)"
onKeyDown={handleKeyDown}
/>

<SettingsInput
id="maxSendMessageLength"
label="Max Send Message Size"
value={maxSendMessageLength}
onChange={onNumericChange('maxSendMessageLength')}
description="Maximum request message size in bytes (-1 for unlimited)"
onKeyDown={handleKeyDown}
/>

<SettingsInput
id="deadline"
label="Deadline (ms)"
value={deadline}
onChange={onNumericChange('deadline')}
description="Per-request deadline in milliseconds, required by many gRPC servers"
onKeyDown={handleKeyDown}
/>

<SettingsInput
id="keepaliveTime"
label="Keepalive Time (ms)"
value={keepaliveTime}
onChange={onNumericChange('keepaliveTime')}
description="Interval between keepalive pings to keep the connection alive"
onKeyDown={handleKeyDown}
/>

<SettingsInput
id="keepaliveTimeout"
label="Keepalive Timeout (ms)"
value={keepaliveTimeout}
onChange={onNumericChange('keepaliveTimeout')}
description="Timeout waiting for a keepalive ping response"
onKeyDown={handleKeyDown}
/>

<SettingsInput
id="clientIdleTimeout"
label="Client Idle Timeout (ms)"
value={clientIdleTimeout}
onChange={onNumericChange('clientIdleTimeout')}
description="Close the connection after being idle for this duration"
onKeyDown={handleKeyDown}
/>

<SettingsInput
id="maxReconnectBackoff"
label="Max Reconnect Backoff (ms)"
value={maxReconnectBackoff}
onChange={onNumericChange('maxReconnectBackoff')}
description="Maximum delay between reconnection attempts after failure"
onKeyDown={handleKeyDown}
/>

<ToggleSelector
checked={includeDefaultValues !== false}
onChange={() => updateSetting('includeDefaultValues', includeDefaultValues === false)}
label="Include Default Values"
description="Include fields with protobuf default values (0, empty string, false) in responses"
size="medium"
/>
</div>
</div>
</div>
);
};

export default GrpcSettingsPane;
Original file line number Diff line number Diff line change
Expand Up @@ -1744,13 +1744,15 @@ export const loadGrpcMethodsFromReflection = (item, collectionUid, url) => async
return reject(error);
}

const settings = itemCopy.draft ? itemCopy.draft.settings : itemCopy.settings;
const { ipcRenderer } = window;
ipcRenderer
.invoke('grpc:load-methods-reflection', {
request: requestItem,
collection: collectionCopy,
environment,
runtimeVariables
runtimeVariables,
settings
})
.then(resolve)
.catch(reject);
Expand Down
4 changes: 3 additions & 1 deletion packages/bruno-app/src/utils/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,14 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const request = item.draft ? item.draft : item;
const settings = item.draft ? item.draft.settings : item.settings;

ipcRenderer.invoke('grpc:start-connection', {
request,
collection,
environment,
runtimeVariables
runtimeVariables,
settings
})
.then(() => {
resolve();
Expand Down
45 changes: 41 additions & 4 deletions packages/bruno-electron/src/ipc/network/grpc-event-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const registerGrpcEventHandlers = (window) => {
});

// Start a new gRPC connection
ipcMain.handle('grpc:start-connection', async (event, { request, collection, environment, runtimeVariables }) => {
ipcMain.handle('grpc:start-connection', async (event, { request, collection, environment, runtimeVariables, settings }) => {
try {
const requestCopy = cloneDeep(request);
const preparedRequest = await prepareGrpcRequest(requestCopy, collection, environment, runtimeVariables, {});
Expand Down Expand Up @@ -219,6 +219,33 @@ const registerGrpcEventHandlers = (window) => {

const includeDirs = getProtobufIncludeDirs(collection);

// Build gRPC channel options from settings
const channelOptionsMap = {
maxReceiveMessageLength: 'grpc.max_receive_message_length',
maxSendMessageLength: 'grpc.max_send_message_length',
keepaliveTime: 'grpc.keepalive_time_ms',
keepaliveTimeout: 'grpc.keepalive_timeout_ms',
clientIdleTimeout: 'grpc.client_idle_timeout_ms',
maxReconnectBackoff: 'grpc.max_reconnect_backoff_ms'
};
const channelOptions = {};
if (settings) {
for (const [key, option] of Object.entries(channelOptionsMap)) {
if (settings[key] != null) {
channelOptions[option] = settings[key];
}
}
}

// Build proto-loader options from settings
const protoOptions = {};
if (settings?.includeDefaultValues != null) {
protoOptions.defaults = settings.includeDefaultValues;
}

// Extract deadline (per-RPC)
const deadline = settings?.deadline ?? null;

// Start gRPC connection with the processed request, certificates, and proxy
await grpcClient.startConnection({
request: preparedRequest,
Expand All @@ -230,7 +257,10 @@ const registerGrpcEventHandlers = (window) => {
pfx,
verifyOptions,
includeDirs,
proxyConfig: grpcProxyConfig
proxyConfig: grpcProxyConfig,
channelOptions,
protoOptions,
deadline
});

sendEvent('grpc:request', preparedRequest.uid, collection.uid, requestSent);
Expand Down Expand Up @@ -312,7 +342,7 @@ const registerGrpcEventHandlers = (window) => {
});

// Load methods from server reflection
ipcMain.handle('grpc:load-methods-reflection', async (event, { request, collection, environment, runtimeVariables }) => {
ipcMain.handle('grpc:load-methods-reflection', async (event, { request, collection, environment, runtimeVariables, settings }) => {
try {
const requestCopy = cloneDeep(request);
const preparedRequest = await prepareGrpcRequest(requestCopy, collection, environment, runtimeVariables);
Expand Down Expand Up @@ -375,6 +405,12 @@ const registerGrpcEventHandlers = (window) => {
});
}

// Build proto-loader options from settings
const protoOptions = {};
if (settings?.includeDefaultValues != null) {
protoOptions.defaults = settings.includeDefaultValues;
}

const methods = await grpcClient.loadMethodsFromReflection({
request: preparedRequest,
collectionUid: collection.uid,
Expand All @@ -385,7 +421,8 @@ const registerGrpcEventHandlers = (window) => {
pfx,
verifyOptions,
sendEvent,
proxyConfig: grpcProxyConfig
proxyConfig: grpcProxyConfig,
protoOptions
});

return { success: true, methods: safeParseJSON(safeStringifyJSON(methods)) };
Expand Down
22 changes: 22 additions & 0 deletions packages/bruno-lang/v2/src/bruToJson.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,28 @@ const sem = grammar.createSemantics().addAttribute('ast', {
_settings.keepAliveInterval = keepAliveInterval;
}

// Parse gRPC-specific numeric settings
const grpcNumericSettings = [
'maxReceiveMessageLength', 'maxSendMessageLength',
'keepaliveTime', 'keepaliveTimeout',
'clientIdleTimeout', 'maxReconnectBackoff', 'deadline'
];
for (const key of grpcNumericSettings) {
if (settings[key] !== undefined) {
const val = parseInt(settings[key], 10);
if (!isNaN(val)) {
_settings[key] = val;
}
}
}

// Parse gRPC includeDefaultValues as boolean
if (settings.includeDefaultValues !== undefined) {
_settings.includeDefaultValues = typeof settings.includeDefaultValues === 'boolean'
? settings.includeDefaultValues
: settings.includeDefaultValues === 'true';
}

return {
settings: _settings
};
Expand Down
Loading
Loading