Repository navigation
feat(proxy): shorthand proxy formats + protocol header mode for bulk import - #6867
Conversation
… (ABI 148) (cheaper-inference#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes cheaper-inference#6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.)
…heaper-inference#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (cheaper-inference#6605).
…7) + production deps bump (cheaper-inference#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes cheaper-inference#6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from cheaper-inference#6605 which passed the smoke. Not this diff.)
…port Supports 6 new shorthand formats alongside the existing pipe-delimited parser: - ip:port - ip:port:user:pass - user:pass@ip:port - user:pass:ip:port - protocol://ip:port - protocol://user:pass@ip:port Protocol header mode: a bare protocol name (http/https/socks5) on its own line sets the default type for subsequent protocol-less shorthand lines. Explicit protocol:// prefix always takes precedence over the header default. Changes: - Rewrite parseBulkProxyImport.ts with parseShorthandLine helper - Use Record<string, true> for static lookup tables (VALID_PROXY_TYPES, VALID_PROXY_STATUSES) per project convention - Add looksLikeHost() heuristic to disambiguate 4-colon format (ip:port:user:pass vs user:pass:ip:port) - Update BULK_IMPORT_TEMPLATE in ProxyRegistryManager.tsx with full documentation and examples for all formats - Update en.json bulkImportDescription to list all supported formats - Add 30 unit tests covering every format, edge cases, and regressions for the existing pipe-delimited path
There was a problem hiding this comment.
Code Review
This pull request expands the proxy bulk import feature by adding support for multiple shorthand formats (such as ip:port, ip:port:user:pass, user:pass@ip:port, user:pass:ip:port, and protocol prefixes) as well as a protocol header mode. It also updates the unit tests to cover these new formats. The code reviewer identified two critical issues in the new parsing logic: first, the looksLikeHost helper is too permissive and can lead to incorrect parsing of 4-colon shorthand formats when the password is a valid port number; second, splitting shorthand lines by : breaks support for IPv6 addresses, introducing a regression. Code suggestions were provided to resolve both issues.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function looksLikeHost(s: string): boolean { | ||
| if (!s) return false; | ||
| // IPv4: four dot-separated octets, each 0–255 | ||
| const ipParts = s.split("."); | ||
| if (ipParts.length === 4 && ipParts.every((o) => /^\d+$/.test(o) && Number(o) >= 0 && Number(o) <= 255)) { | ||
| return true; | ||
| } | ||
| // Hostname: alphanumeric + dots/hyphens, at least one char | ||
| return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s); | ||
| } |
There was a problem hiding this comment.
The current implementation of looksLikeHost is too permissive for hostname validation. Since it matches any alphanumeric string (e.g., "myuser"), it can cause incorrect parsing of 4-colon shorthand formats like user:pass:ip:port when the password is a valid port number (e.g., myuser:1234:1.2.3.4:8080). In this case, looksLikeHost("myuser") returns true and isPort1 is true, causing it to be incorrectly parsed as ip:port:user:pass with host "myuser" and port 1234.\n\nTo prevent this, looksLikeHost should require a dot in the hostname (or explicitly match localhost and bracketed IPv6 addresses).
function looksLikeHost(s: string): boolean {
if (!s) return false;
const lower = s.toLowerCase();
if (lower === "localhost") return true;
// Bracketed IPv6
if (s.startsWith("[") && s.endsWith("]")) {
return true;
}
// IPv4: four dot-separated octets, each 0–255
const ipParts = s.split(".");
if (ipParts.length === 4 && ipParts.every((o) => /^\d+$/.test(o) && Number(o) >= 0 && Number(o) <= 255)) {
return true;
}
// Hostname: must contain at least one dot, and match alphanumeric + dots/hyphens
return s.includes(".") && /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s);
}| // No @ — split by colon to determine which colon-delimited format | ||
| const colonParts = working.split(":").map((p) => p.trim()); | ||
|
|
||
| if (colonParts.length === 2) { | ||
| // host:port | ||
| const [host, portStr] = colonParts; | ||
| return pushShorthandEntry(entries, errors, lineNum, host, portStr, "", "", type); | ||
| } | ||
|
|
||
| if (colonParts.length === 4) { | ||
| // Two possibilities: ip:port:user:pass OR user:pass:ip:port | ||
| // Require the "host" slot to look like an IP/hostname AND the "port" slot to be a valid port. | ||
| const isPort1 = /^\d+$/.test(colonParts[1]) && Number(colonParts[1]) >= 1 && Number(colonParts[1]) <= 65535; | ||
| const isPort3 = /^\d+$/.test(colonParts[3]) && Number(colonParts[3]) >= 1 && Number(colonParts[3]) <= 65535; | ||
| const hostLooksLikePart0 = looksLikeHost(colonParts[0]); | ||
| const hostLooksLikePart2 = looksLikeHost(colonParts[2]); | ||
|
|
||
| if (hostLooksLikePart0 && isPort1) { | ||
| // ip:port:user:pass | ||
| const [host, portStr, username, password] = colonParts; | ||
| return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type); | ||
| } | ||
| if (hostLooksLikePart2 && isPort3) { | ||
| // user:pass:ip:port | ||
| const [username, password, host, portStr] = colonParts; | ||
| return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type); | ||
| } | ||
| // Can't determine format | ||
| errors.push({ line: lineNum, reason: "bulkImportErrorInvalidPort" }); | ||
| return false; | ||
| } | ||
|
|
||
| // Unknown colon format | ||
| errors.push({ line: lineNum, reason: "bulkImportErrorMissingHost" }); | ||
| return false; |
There was a problem hiding this comment.
Splitting the shorthand proxy line by : breaks support for IPv6 addresses (e.g., [2001:db8::1]:8080 or user:pass:[2001:db8::1]:8080), which are split into more than 4 parts and fail to parse. This is a regression from the previous simple HOST:PORT parser which used lastIndexOf(":").\n\nWe can handle bracketed IPv6 hosts explicitly before splitting by : to preserve full IPv6 compatibility.
// No @ — check for bracketed IPv6 hosts first to avoid splitting on IPv6 colons
if (working.startsWith("[")) {
const closeBracketIdx = working.indexOf("]");
if (closeBracketIdx > 0 && working.charAt(closeBracketIdx + 1) === ":") {
const host = working.slice(0, closeBracketIdx + 1).trim();
const rest = working.slice(closeBracketIdx + 2).trim();
const restParts = rest.split(":").map((p) => p.trim());
if (restParts.length === 1) {
// [ipv6]:port
return pushShorthandEntry(entries, errors, lineNum, host, restParts[0], "", "", type);
}
if (restParts.length === 3) {
// [ipv6]:port:user:pass
return pushShorthandEntry(entries, errors, lineNum, host, restParts[0], restParts[1], restParts[2], type);
}
}
}
const openBracketIdx = working.indexOf(":[");
if (openBracketIdx > 0) {
const closeBracketIdx = working.indexOf("]", openBracketIdx);
if (closeBracketIdx > 0 && working.charAt(closeBracketIdx + 1) === ":") {
const authPart = working.slice(0, openBracketIdx).trim();
const host = working.slice(openBracketIdx + 1, closeBracketIdx + 1).trim();
const portStr = working.slice(closeBracketIdx + 2).trim();
const authParts = authPart.split(":").map((p) => p.trim());
if (authParts.length === 2) {
// user:pass:[ipv6]:port
return pushShorthandEntry(entries, errors, lineNum, host, portStr, authParts[0], authParts[1], type);
}
}
}
const colonParts = working.split(":").map((p) => p.trim());
if (colonParts.length === 2) {
// host:port
const [host, portStr] = colonParts;
return pushShorthandEntry(entries, errors, lineNum, host, portStr, "", "", type);
}
if (colonParts.length === 4) {
// Two possibilities: ip:port:user:pass OR user:pass:ip:port
// Require the "host" slot to look like an IP/hostname AND the "port" slot to be a valid port.
const isPort1 = /^\d+$/.test(colonParts[1]) && Number(colonParts[1]) >= 1 && Number(colonParts[1]) <= 65535;
const isPort3 = /^\d+$/.test(colonParts[3]) && Number(colonParts[3]) >= 1 && Number(colonParts[3]) <= 65535;
const hostLooksLikePart0 = looksLikeHost(colonParts[0]);
const hostLooksLikePart2 = looksLikeHost(colonParts[2]);
if (hostLooksLikePart0 && isPort1) {
// ip:port:user:pass
const [host, portStr, username, password] = colonParts;
return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type);
}
if (hostLooksLikePart2 && isPort3) {
// user:pass:ip:port
const [username, password, host, portStr] = colonParts;
return pushShorthandEntry(entries, errors, lineNum, host, portStr, username, password, type);
}
// Can't determine format
errors.push({ line: lineNum, reason: "bulkImportErrorInvalidPort" });
return false;
}
// Unknown colon format
errors.push({ line: lineNum, reason: "bulkImportErrorMissingHost" });
return false;|
Nice consolidation — removing the duplicate inline parser in ProxyRegistryManager.tsx so the shared parseBulkProxyImport.ts module (which already documented HOST:PORT shorthand in the i18n string) actually runs at all is a real bug fix on its own. The 6 new shorthand formats + protocol-header mode are well covered by 30 passing tests (verified locally: |
Resolve conflict in ProxyRegistryManager.tsx by keeping both imports (bulk-import parser from this PR + pool-strategy options from release).
c4c8af3
into
cheaper-inference:release/v3.8.47
…import (cheaper-inference#6867) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (cheaper-inference#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes cheaper-inference#6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (cheaper-inference#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (cheaper-inference#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (cheaper-inference#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes cheaper-inference#6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from cheaper-inference#6605 which passed the smoke. Not this diff.) * feat(proxy): add shorthand formats + protocol header mode for bulk import Supports 6 new shorthand formats alongside the existing pipe-delimited parser: - ip:port - ip:port:user:pass - user:pass@ip:port - user:pass:ip:port - protocol://ip:port - protocol://user:pass@ip:port Protocol header mode: a bare protocol name (http/https/socks5) on its own line sets the default type for subsequent protocol-less shorthand lines. Explicit protocol:// prefix always takes precedence over the header default. Changes: - Rewrite parseBulkProxyImport.ts with parseShorthandLine helper - Use Record<string, true> for static lookup tables (VALID_PROXY_TYPES, VALID_PROXY_STATUSES) per project convention - Add looksLikeHost() heuristic to disambiguate 4-colon format (ip:port:user:pass vs user:pass:ip:port) - Update BULK_IMPORT_TEMPLATE in ProxyRegistryManager.tsx with full documentation and examples for all formats - Update en.json bulkImportDescription to list all supported formats - Add 30 unit tests covering every format, edge cases, and regressions for the existing pipe-delimited path --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
…import (cheaper-inference#6867) * fix(electron): bump electron 42→43 + build better-sqlite3 from source (ABI 148) (cheaper-inference#6605) fix(electron): bump electron 42→43 + rebuild better-sqlite3 from source against the Electron ABI (148). Electron 43 raises NODE_MODULE_VERSION to 148; better-sqlite3@12.11.1 has no electron-v148 prebuild, so the packaged app died with 'Nenhum driver SQLite disponível'. prepare-electron-standalone now compiles better-sqlite3 from source against the electron headers into build/Release (where 'bindings' resolves it). Validated by Electron Package Smoke (green) + local (node_register_module_v148). Supersedes cheaper-inference#6378. (--admin: the only reds are SonarQube/SonarCloud failing on a coverage-report artifact digest-mismatch — a GitHub Actions infra flake, not this diff; Sonar is green on main and the diff touches only the electron build.) * deps: bump the development group across 1 directory with 6 updates (cheaper-inference#6588) deps: bump the development group (6 updates). Rebased onto current main; all checks green after the electron-smoke fix (cheaper-inference#6605). * fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump (cheaper-inference#6620) fix(proxy): force CONNECT tunnel for HTTP proxied requests (undici 8.7) + production deps bump. undici 8.6+ changed ProxyAgent to forward plain-HTTP via request-proxy instead of CONNECT, breaking OAuth refresh through a connection proxy (501). proxyDispatcher now passes proxyTunnel:true. Validated: Unit Tests 3/8 (the OAuth-proxy test) green, new regression test green (fails without the fix on undici 8.7), SonarQube green. Supersedes cheaper-inference#6380. (--admin: the only red is Electron Package Smoke failing on a next-build artifact 'digest-mismatch' — a GitHub Actions infra flake corrupting the asar ('file data stream has unexpected number of bytes'); the better-sqlite3 rebuild itself succeeded (gyp ok) and the electron path is unchanged from cheaper-inference#6605 which passed the smoke. Not this diff.) * feat(proxy): add shorthand formats + protocol header mode for bulk import Supports 6 new shorthand formats alongside the existing pipe-delimited parser: - ip:port - ip:port:user:pass - user:pass@ip:port - user:pass:ip:port - protocol://ip:port - protocol://user:pass@ip:port Protocol header mode: a bare protocol name (http/https/socks5) on its own line sets the default type for subsequent protocol-less shorthand lines. Explicit protocol:// prefix always takes precedence over the header default. Changes: - Rewrite parseBulkProxyImport.ts with parseShorthandLine helper - Use Record<string, true> for static lookup tables (VALID_PROXY_TYPES, VALID_PROXY_STATUSES) per project convention - Add looksLikeHost() heuristic to disambiguate 4-colon format (ip:port:user:pass vs user:pass:ip:port) - Update BULK_IMPORT_TEMPLATE in ProxyRegistryManager.tsx with full documentation and examples for all formats - Update en.json bulkImportDescription to list all supported formats - Add 30 unit tests covering every format, edge cases, and regressions for the existing pipe-delimited path --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Summary
Adds support for 6 new shorthand proxy formats and a protocol-header mode to the proxy Bulk Import feature, alongside the existing pipe-delimited format (fully backward-compatible).
Problem
The proxy bulk import only accepted the pipe-delimited format (
NAME|HOST|PORT|USERNAME|PASSWORD|TYPE|REGION|STATUS|NOTES). Users copying proxy lists from common proxy providers (which useip:port:user:pass,user:pass@ip:port,protocol://user:pass@ip:port, etc.) had to manually reformat each line.The
en.jsondescription already mentionedHOST:PORTshorthand support, but the inline parser inProxyRegistryManager.tsxnever implemented it — it only handled pipe-delimited lines.Additionally,
ProxyRegistryManager.tsxhad a duplicate inline parser that shadowed the extractedparseBulkProxyImport.tsmodule. The inline copy only supported pipe-delimited format, so the shorthand parsing logic in the shared module was never called at runtime.Changes
1.
parseBulkProxyImport.ts— rewritten parserSupports all formats (one proxy per line):
proxy-us|138.99.147.218|50101|myuser|mypass|socks5|US-East|active|US proxyip:port138.99.147.218:50101ip:port:user:pass138.99.147.218:50101:myuser:mypassuser:pass@ip:portmyuser:mypass@138.99.147.218:50101user:pass:ip:portmyuser:mypass:138.99.147.218:50101protocol://ip:porthttp://10.0.0.50:8080protocol://user:pass@ip:porthttps://admin:secret@proxy.example.com:443Protocol header mode: A bare protocol name (
http,https,socks5) on its own line sets the default type for all subsequent shorthand lines without an explicitprotocol://prefix. The per-lineprotocol://prefix always takes precedence over the header default.4-colon disambiguation: For
a:b:c:dformat, the parser checks ifparts[0]looks like an IP/hostname andparts[1]is a valid port →ip:port:user:pass. Otherwise ifparts[2]looks like an IP/hostname andparts[3]is a valid port →user:pass:ip:port.Default type change: Shorthand
ip:port(no protocol) now defaults tosocks5instead ofhttpfor consistency with the pipe-delimited default.2.
ProxyRegistryManager.tsx— removed inline parser duplicateparseBulkImportTextfunction,VALID_TYPES,VALID_STATUSES, and duplicateParsedProxyEntry/ParseErrortypesimport { parseBulkImportText, type ParsedProxyEntry, type ParseError } from "./parseBulkProxyImport"BULK_IMPORT_TEMPLATEcomment block with full documentation and examples for all 3 format groups3.
en.json— updated descriptionUpdated
bulkImportDescriptionto list all supported formats.4. Tests —
proxy-registry-manager.test.tsExpanded from 13 → 30 tests covering:
Commands ran
Changed test files
tests/unit/proxy-registry-manager.test.ts— expanded from 13 to 30 testsCoverage result
All metrics exceed the 60% threshold (statements, lines, functions, branches).