diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js
index 7ea16f177d5..e7b0088dc68 100644
--- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js
@@ -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';
@@ -41,6 +42,9 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
case 'auth': {
return ;
}
+ case 'settings': {
+ return ;
+ }
case 'docs': {
return ;
}
@@ -90,6 +94,11 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
label: 'Auth',
indicator: auth?.mode && auth.mode !== 'none' ? : null
},
+ {
+ key: 'settings',
+ label: 'Settings',
+ indicator: null
+ },
{
key: 'docs',
label: 'Docs',
diff --git a/packages/bruno-app/src/components/RequestPane/GrpcSettingsPane/index.js b/packages/bruno-app/src/components/RequestPane/GrpcSettingsPane/index.js
new file mode 100644
index 00000000000..d988de97348
--- /dev/null
+++ b/packages/bruno-app/src/components/RequestPane/GrpcSettingsPane/index.js
@@ -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 (
+
+
Configure gRPC channel and request settings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updateSetting('includeDefaultValues', includeDefaultValues === false)}
+ label="Include Default Values"
+ description="Include fields with protobuf default values (0, empty string, false) in responses"
+ size="medium"
+ />
+
+
+
+ );
+};
+
+export default GrpcSettingsPane;
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 89120abf51b..18c5ab03894 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -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);
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index b1a265276fc..5e9620d1f89 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -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();
diff --git a/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js b/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js
index ac3f86e43ac..42cda10b60b 100644
--- a/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js
+++ b/packages/bruno-electron/src/ipc/network/grpc-event-handlers.js
@@ -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, {});
@@ -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,
@@ -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);
@@ -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);
@@ -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,
@@ -385,7 +421,8 @@ const registerGrpcEventHandlers = (window) => {
pfx,
verifyOptions,
sendEvent,
- proxyConfig: grpcProxyConfig
+ proxyConfig: grpcProxyConfig,
+ protoOptions
});
return { success: true, methods: safeParseJSON(safeStringifyJSON(methods)) };
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index ecab5eee61a..ae2765c2548 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -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
};
diff --git a/packages/bruno-requests/src/grpc/grpc-client.js b/packages/bruno-requests/src/grpc/grpc-client.js
index 7d2b7b5fdb7..55ef1bb4335 100644
--- a/packages/bruno-requests/src/grpc/grpc-client.js
+++ b/packages/bruno-requests/src/grpc/grpc-client.js
@@ -34,6 +34,16 @@ const configOptions = {
json: true
};
+/**
+ * Default gRPC channel options.
+ * Sets unlimited message sizes since Bruno is a client-side tool
+ * and should not impose arbitrary limits on responses.
+ */
+const DEFAULT_CHANNEL_OPTIONS = {
+ 'grpc.max_receive_message_length': -1,
+ 'grpc.max_send_message_length': -1
+};
+
const reflectionServices = ['grpc.reflection.v1alpha.ServerReflection', 'grpc.reflection.v1.ServerReflection'];
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
@@ -431,7 +441,7 @@ class GrpcClient {
* @returns {Promise} Whether methods were successfully refreshed
* @private
*/
- async #refreshMethods({ url, headers, protoPath, collectionPath, collectionUid, certificates = {}, verifyOptions, includeDirs = [], proxyConfig }) {
+ async #refreshMethods({ url, headers, protoPath, collectionPath, collectionUid, certificates = {}, verifyOptions, includeDirs = [], proxyConfig, protoOptions = {} }) {
try {
// Try reflection first if no proto path is specified
if (!protoPath) {
@@ -445,7 +455,8 @@ class GrpcClient {
pfx: certificates.pfx,
verifyOptions,
sendEvent: () => {}, // No-op for refresh
- proxyConfig
+ proxyConfig,
+ protoOptions
});
return true;
}
@@ -453,7 +464,7 @@ class GrpcClient {
// Try proto file if available
if (protoPath) {
const absoluteProtoPath = nodePath.resolve(collectionPath, protoPath);
- await this.loadMethodsFromProtoFile(absoluteProtoPath, includeDirs);
+ await this.loadMethodsFromProtoFile(absoluteProtoPath, includeDirs, protoOptions);
return true;
}
@@ -498,13 +509,14 @@ class GrpcClient {
/**
* Handle unary responses
*/
- #handleUnaryResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid }) {
+ #handleUnaryResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid, callOptions = {} }) {
const rpc = client.makeUnaryRequest(
requestPath,
method.requestSerialize,
method.responseDeserialize,
messages[0],
metadata,
+ callOptions,
(error, res) => {
this.eventCallback('grpc:response', requestId, collectionUid, { error, res });
}
@@ -514,12 +526,13 @@ class GrpcClient {
setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc, () => this.#removeConnection(requestId));
}
- #handleClientStreamingResponse({ client, requestId, requestPath, method, metadata, collectionUid }) {
+ #handleClientStreamingResponse({ client, requestId, requestPath, method, metadata, collectionUid, callOptions = {} }) {
const rpc = client.makeClientStreamRequest(
requestPath,
method.requestSerialize,
method.responseDeserialize,
metadata,
+ callOptions,
(error, res) => {
this.eventCallback('grpc:response', requestId, collectionUid, { error, res });
}
@@ -529,7 +542,7 @@ class GrpcClient {
setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc, () => this.#removeConnection(requestId));
}
- #handleServerStreamingResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid }) {
+ #handleServerStreamingResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid, callOptions = {} }) {
const message = messages[0];
const rpc = client.makeServerStreamRequest(
requestPath,
@@ -537,6 +550,7 @@ class GrpcClient {
method.responseDeserialize,
message,
metadata,
+ callOptions,
(error, res) => {
this.eventCallback('grpc:response', requestId, collectionUid, { error, res });
}
@@ -546,12 +560,13 @@ class GrpcClient {
setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc, () => this.#removeConnection(requestId));
}
- #handleBidiStreamingResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid }) {
+ #handleBidiStreamingResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid, callOptions = {} }) {
const rpc = client.makeBidiStreamRequest(
requestPath,
method.requestSerialize,
method.responseDeserialize,
- metadata
+ metadata,
+ callOptions
);
this.#addConnection(requestId, { rpc, client });
@@ -592,7 +607,9 @@ class GrpcClient {
verifyOptions,
channelOptions = {},
includeDirs = [],
- proxyConfig
+ proxyConfig,
+ protoOptions = {},
+ deadline
}) {
const credentials = this.#getChannelCredentials({
url: request.url,
@@ -630,7 +647,8 @@ class GrpcClient {
},
verifyOptions,
includeDirs,
- proxyConfig
+ proxyConfig,
+ protoOptions
});
if (!refreshSuccess) {
@@ -655,7 +673,7 @@ class GrpcClient {
// Resolve proxy target and channel options
const { targetHost, proxyChannelOptions } = this.#resolveProxyTarget(host, proxyConfig);
- const mergedChannelOptions = { ...channelOptions, ...proxyChannelOptions };
+ const mergedChannelOptions = { ...DEFAULT_CHANNEL_OPTIONS, ...channelOptions, ...proxyChannelOptions };
if (userAgentValue && !channelOptions?.['grpc.primary_user_agent']) {
mergedChannelOptions['grpc.primary_user_agent'] = userAgentValue;
}
@@ -666,6 +684,12 @@ class GrpcClient {
throw new Error('Failed to create client');
}
+ // Build per-RPC call options (e.g. deadline)
+ const callOptions = {};
+ if (deadline != null && deadline > 0) {
+ callOptions.deadline = new Date(Date.now() + deadline);
+ }
+
let messages = request.body.grpc;
try {
messages = messages.map(({ content }) => safeJsonParse(content, 'message content'));
@@ -693,7 +717,8 @@ class GrpcClient {
requestPath,
method,
messages,
- metadata
+ metadata,
+ callOptions
});
}
@@ -748,7 +773,8 @@ class GrpcClient {
verifyOptions,
sendEvent,
channelOptions = {},
- proxyConfig
+ proxyConfig,
+ protoOptions = {}
}) {
const { host, path } = getParsedGrpcUrlObject(request.url);
@@ -762,7 +788,7 @@ class GrpcClient {
// Resolve proxy target and channel options
const { targetHost, proxyChannelOptions } = this.#resolveProxyTarget(host, proxyConfig);
- const mergedChannelOptions = { ...channelOptions, ...proxyChannelOptions };
+ const mergedChannelOptions = { ...DEFAULT_CHANNEL_OPTIONS, ...channelOptions, ...proxyChannelOptions };
if (userAgentValue && !channelOptions?.['grpc.primary_user_agent']) {
mergedChannelOptions['grpc.primary_user_agent'] = userAgentValue;
}
@@ -786,12 +812,15 @@ class GrpcClient {
const { client, services, callOptions } = await this.#getReflectionClient(targetHost, credentials, metadata, mergedChannelOptions);
reflectionClient = client;
+ const mergedProtoOptions = { ...configOptions, ...protoOptions };
const methods = [];
for (const service of services) {
if (reflectionServices.includes(service)) {
continue;
}
- const m = await client.listMethods(service, callOptions);
+ const descriptor = await client.getDescriptorBySymbol(service, callOptions);
+ const packageObject = descriptor.getPackageObject(mergedProtoOptions);
+ const m = client.getServiceMethods(packageObject, service);
methods.push(...m);
}
@@ -817,8 +846,9 @@ class GrpcClient {
}
}
- async loadMethodsFromProtoFile(filePath, includeDirs = []) {
- const protoDefinition = await protoLoader.load(filePath, { ...configOptions, includeDirs });
+ async loadMethodsFromProtoFile(filePath, includeDirs = [], protoOptions = {}) {
+ const mergedConfigOptions = { ...configOptions, ...protoOptions };
+ const protoDefinition = await protoLoader.load(filePath, { ...mergedConfigOptions, includeDirs });
const methods = Object.values(protoDefinition)
.filter((definition) => !definition?.format)
.flatMap(Object.values);
diff --git a/packages/bruno-requests/src/grpc/grpc-client.spec.js b/packages/bruno-requests/src/grpc/grpc-client.spec.js
index 74b6f593117..9ea8b27aa4b 100644
--- a/packages/bruno-requests/src/grpc/grpc-client.spec.js
+++ b/packages/bruno-requests/src/grpc/grpc-client.spec.js
@@ -8,13 +8,17 @@ let capturedHost = null;
// Mock GrpcReflection to capture options
const mockListServices = jest.fn().mockResolvedValue(['test.Service']);
-const mockListMethods = jest.fn().mockResolvedValue([
+const mockGetDescriptorBySymbol = jest.fn().mockResolvedValue({
+ getPackageObject: jest.fn().mockReturnValue({})
+});
+const mockGetServiceMethods = jest.fn().mockReturnValue([
{
- path: '/test.Service/TestMethod',
+ name: 'TestMethod',
definition: {
requestStream: false,
responseStream: false
- }
+ },
+ path: '/test.Service/TestMethod'
}
]);
@@ -24,7 +28,8 @@ jest.mock('grpc-js-reflection-client', () => ({
capturedHost = host;
return {
listServices: mockListServices,
- listMethods: mockListMethods
+ getDescriptorBySymbol: mockGetDescriptorBySymbol,
+ getServiceMethods: mockGetServiceMethods
};
})
}));
diff --git a/packages/bruno-schema-types/src/collection/item.ts b/packages/bruno-schema-types/src/collection/item.ts
index bdf3b334601..cb5d786cd3b 100644
--- a/packages/bruno-schema-types/src/collection/item.ts
+++ b/packages/bruno-schema-types/src/collection/item.ts
@@ -25,7 +25,18 @@ export interface WebSocketItemSettings {
} | null;
}
-export type ItemSettings = HttpItemSettings | WebSocketItemSettings | null;
+export interface GrpcItemSettings {
+ maxReceiveMessageLength?: number | null;
+ maxSendMessageLength?: number | null;
+ keepaliveTime?: number | null;
+ keepaliveTimeout?: number | null;
+ clientIdleTimeout?: number | null;
+ maxReconnectBackoff?: number | null;
+ deadline?: number | null;
+ includeDefaultValues?: boolean | null;
+}
+
+export type ItemSettings = HttpItemSettings | WebSocketItemSettings | GrpcItemSettings | null;
export interface Item {
uid: UID;