mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: add default http protocol when URL scheme is missing (#7786)
--------- Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com> Co-authored-by: Sid <siddharth@usebruno.com>
This commit is contained in:
@@ -16,13 +16,12 @@ const path = require('path');
|
||||
const { parseDataFromResponse } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies } = require('../utils/cookies');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2, applyOAuth1ToRequest } = require('@usebruno/requests');
|
||||
const { getCACertificates, transformProxyConfig } = require('@usebruno/requests');
|
||||
const { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2');
|
||||
const tokenStore = require('../store/tokenStore');
|
||||
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData, extractBoundaryFromContentType } = require('@usebruno/common').utils;
|
||||
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData, extractBoundaryFromContentType, hasExplicitScheme } = require('@usebruno/common').utils;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -344,7 +343,7 @@ const runSingleRequest = async function (
|
||||
request.url = encodeUrl(request.url);
|
||||
}
|
||||
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
if (!hasExplicitScheme(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,14 +68,18 @@ jest.mock('../../src/store/tokenStore', () => ({
|
||||
|
||||
// Default: no prompt variables detected
|
||||
const mockExtractPromptVariables = jest.fn(() => []);
|
||||
jest.mock('@usebruno/common', () => ({
|
||||
jest.mock('@usebruno/common', () => {
|
||||
const ogUtils = jest.requireActual('@usebruno/common').utils;
|
||||
return {
|
||||
utils: {
|
||||
encodeUrl: jest.fn((u) => u),
|
||||
buildFormUrlEncodedPayload: jest.fn(),
|
||||
extractPromptVariables: mockExtractPromptVariables,
|
||||
isFormData: jest.fn(() => false)
|
||||
isFormData: jest.fn(() => false),
|
||||
hasExplicitScheme: ogUtils.hasExplicitScheme
|
||||
}
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
const prepareRequest = require('../../src/runner/prepare-request');
|
||||
const { makeAxiosInstance } = require('../../src/utils/axios-instance');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
hasExplicitScheme,
|
||||
encodeUrl,
|
||||
parseQueryParams,
|
||||
buildQueryString,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { encodeUrl, parseQueryParams, buildQueryString } from './index';
|
||||
import { encodeUrl, parseQueryParams, buildQueryString, hasExplicitScheme } from './index';
|
||||
|
||||
describe('encodeUrl', () => {
|
||||
describe('basic functionality', () => {
|
||||
@@ -260,3 +260,49 @@ describe('buildQueryString', () => {
|
||||
expect(result).toBe('seat=&table=2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasExplicitScheme', () => {
|
||||
// should return false
|
||||
const noScheme: [string, string][] = [
|
||||
['bare hostname', 'test-domain'],
|
||||
['localhost', 'localhost'],
|
||||
['localhost:port (key regression)', 'localhost:8080'],
|
||||
['localhost:port/path', 'localhost:8080/path'],
|
||||
['127.0.0.1:port', '127.0.0.1:3000'],
|
||||
['bare IP', '192.168.1.1'],
|
||||
['IP:port', '192.168.1.1:8080'],
|
||||
['hostname with path', 'example.com/api/v1']
|
||||
];
|
||||
|
||||
for (const [label, url] of noScheme) {
|
||||
it(`false (no explicit scheme) — ${label}`, () => {
|
||||
expect(hasExplicitScheme(url)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
// should return true
|
||||
const withScheme: [string, string][] = [
|
||||
['http://', 'http://example.com'],
|
||||
['https://', 'https://example.com'],
|
||||
['ftp://', 'ftp://test-domain'],
|
||||
['ws://', 'ws://example.com/socket'],
|
||||
['wss://', 'wss://example.com/socket'],
|
||||
['custom scheme', 'myapp://deep-link']
|
||||
];
|
||||
|
||||
for (const [label, url] of withScheme) {
|
||||
it(`true (has explicit scheme) — ${label}`, () => {
|
||||
expect(hasExplicitScheme(url)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
it('{{baseUrl}}/api — no scheme injection for template variables', async () => {
|
||||
const url = '{{baseUrl}}/api/v1';
|
||||
expect(hasExplicitScheme(url)).toBe(false);
|
||||
});
|
||||
|
||||
it('{{baseUrl}} alone — no scheme injection for template variables', async () => {
|
||||
const url = '{{baseUrl}}';
|
||||
expect(hasExplicitScheme(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
/**
|
||||
* Returns true when `url` already carries an explicit network scheme.
|
||||
*
|
||||
* Per the WHATWG URL Standard, all network-fetch schemes (http, https, ftp,
|
||||
* ws, wss, file) require "://" — the authority component is mandatory.
|
||||
* This means "localhost:8080" is NOT a scheme: the colon separates host from
|
||||
* port, so callers should prepend "http://" to it.
|
||||
*
|
||||
* The scheme character set (ASCII alpha/digit/+/-/.) follows the WHATWG URL
|
||||
* scheme-state parser, which accepts the same characters as all major browsers.
|
||||
* @see https://url.spec.whatwg.org/#scheme-state
|
||||
*
|
||||
* @example
|
||||
* hasExplicitScheme('https://example.com') // true
|
||||
* hasExplicitScheme('ftp://files.example') // true
|
||||
* hasExplicitScheme('localhost:8080') // false — port colon, not scheme
|
||||
* hasExplicitScheme('example.com/api') // false — no scheme at all
|
||||
*/
|
||||
function hasExplicitScheme(url: string): boolean {
|
||||
// All WHATWG network schemes require authority ("://").
|
||||
const authorityStart = url.indexOf('://');
|
||||
if (authorityStart < 1) return false;
|
||||
|
||||
const scheme = url.slice(0, authorityStart);
|
||||
|
||||
// WHATWG URL scheme-state: first character must be ASCII alpha.
|
||||
const first = scheme[0];
|
||||
const isAlpha = (c: string) => {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
};
|
||||
|
||||
if (!isAlpha(first)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remaining characters must be ASCII alphanumeric, "+", "-", or ".".
|
||||
const isSchemeChar = (c: string) => {
|
||||
return isAlpha(c) || (c >= '0' && c <= '9') || c === '+' || c === '-' || c === '.';
|
||||
};
|
||||
return scheme.slice(1).split('').every(isSchemeChar);
|
||||
}
|
||||
|
||||
interface QueryParam {
|
||||
name: string;
|
||||
value?: string;
|
||||
@@ -97,6 +139,7 @@ const stripOrigin = (url: string): string => {
|
||||
};
|
||||
|
||||
export {
|
||||
hasExplicitScheme,
|
||||
encodeUrl,
|
||||
parseQueryParams,
|
||||
buildQueryString,
|
||||
|
||||
@@ -10,7 +10,7 @@ const { ipcMain } = require('electron');
|
||||
const { each, get, extend, cloneDeep, merge } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime, formatErrorWithContextV2 } = require('@usebruno/js');
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { encodeUrl, hasExplicitScheme } = require('@usebruno/common').utils;
|
||||
const { extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper');
|
||||
@@ -108,9 +108,8 @@ const configureRequest = async (
|
||||
collectionPath,
|
||||
globalEnvironmentVariables
|
||||
) => {
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
const hasVariables = request.url.startsWith('{{');
|
||||
if (!hasVariables && !protocolRegex.test(request.url)) {
|
||||
if (!hasVariables && !hasExplicitScheme(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,80 @@
|
||||
const { configureRequest } = require('../../src/ipc/network/index');
|
||||
|
||||
describe('index: configureRequest', () => {
|
||||
it('Should add \'http://\' to the URL if no protocol is specified', async () => {
|
||||
const request = { method: 'GET', url: 'test-domain', body: {} };
|
||||
// Integration tests: full configureRequest (URL must survive cookie-jar parse)
|
||||
describe('index: configureRequest — URL normalization', () => {
|
||||
it('prepends http:// to localhost:port', async () => {
|
||||
const request = { method: 'GET', url: 'localhost:8080', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('http://test-domain');
|
||||
expect(request.url).toEqual('http://localhost:8080');
|
||||
});
|
||||
|
||||
it('Should NOT add \'http://\' to the URL if a protocol is specified', async () => {
|
||||
it('prepends http:// to localhost', async () => {
|
||||
const request = { method: 'GET', url: 'localhost', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('http://localhost');
|
||||
});
|
||||
|
||||
it('prepends http:// to 127.0.0.1:port', async () => {
|
||||
const request = { method: 'GET', url: '127.0.0.1:3000', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('http://127.0.0.1:3000');
|
||||
});
|
||||
|
||||
it('prepends http:// to example.com/api/v1', async () => {
|
||||
const request = { method: 'GET', url: 'example.com/api/v1', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('http://example.com/api/v1');
|
||||
});
|
||||
|
||||
it('does not prepend http:// to http://example.com', async () => {
|
||||
const request = { method: 'GET', url: 'http://example.com', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('http://example.com');
|
||||
});
|
||||
|
||||
it('does not prepend http:// to https://example.com', async () => {
|
||||
const request = { method: 'GET', url: 'https://example.com', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('https://example.com');
|
||||
});
|
||||
|
||||
it('does not prepend http:// to ftp://test-domain', async () => {
|
||||
const request = { method: 'GET', url: 'ftp://test-domain', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('ftp://test-domain');
|
||||
});
|
||||
|
||||
it('does not prepend http:// to ws://example.com/socket', async () => {
|
||||
const request = { method: 'GET', url: 'ws://example.com/socket', body: {} };
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('ws://example.com/socket');
|
||||
});
|
||||
|
||||
describe('with variables in the url and no interpolation values', () => {
|
||||
it('does not prepend http:// to {{baseUrl}}/api/v1 (template variable)', async () => {
|
||||
const url = '{{baseUrl}}/api/v1';
|
||||
const request = { method: 'GET', url, body: {} };
|
||||
expect.assertions(2);
|
||||
try {
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('Invalid URL');
|
||||
} finally {
|
||||
expect(request.url).toEqual(url);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not prepend http:// to {{baseUrl}} alone (template variable)', async () => {
|
||||
const url = '{{baseUrl}}';
|
||||
const request = { method: 'GET', url, body: {} };
|
||||
expect.assertions(2);
|
||||
try {
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('Invalid URL');
|
||||
} finally {
|
||||
expect(request.url).toEqual(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
16
packages/bruno-tests/collection/url-serialization/scheme.bru
Normal file
16
packages/bruno-tests/collection/url-serialization/scheme.bru
Normal file
@@ -0,0 +1,16 @@
|
||||
meta {
|
||||
name: scheme
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: localhost:8081/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
res.body: eq pong
|
||||
}
|
||||
Reference in New Issue
Block a user