-
-
Notifications
You must be signed in to change notification settings - Fork 118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: support IPv6 during ClientRequest options parsing #489
Changes from all commits
0553830
c29867a
be6eddc
021a21e
5fa07e0
32e0dd7
0800de5
8aad96f
f0ada96
1ae4e38
d28232a
2478a73
6f351ca
184ce04
3f4529a
718648b
537448b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ export type ResolvedRequestOptions = RequestOptions & RequestSelf | |
|
||
export const DEFAULT_PATH = '/' | ||
const DEFAULT_PROTOCOL = 'http:' | ||
const DEFAULT_HOST = 'localhost' | ||
const DEFAULT_HOSTNAME = 'localhost' | ||
const SSL_PORT = 443 | ||
|
||
function getAgent( | ||
|
@@ -50,15 +50,6 @@ function getPortByRequestOptions( | |
return Number(options.port) | ||
} | ||
|
||
// Extract the port from the hostname. | ||
if (options.hostname != null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 What kind of hackery were we supporting with this. Ditto, let's remove it as well. Fewer hacks, everyone wins. |
||
const [, extractedPort] = options.hostname.match(/:(\d+)$/) || [] | ||
|
||
if (extractedPort != null) { | ||
return Number(extractedPort) | ||
} | ||
} | ||
|
||
// Otherwise, try to resolve port from the agent. | ||
const agent = getAgent(options) | ||
|
||
|
@@ -75,17 +66,6 @@ function getPortByRequestOptions( | |
return undefined | ||
} | ||
|
||
function getHostByRequestOptions(options: ResolvedRequestOptions): string { | ||
const { hostname, host } = options | ||
|
||
// If the hostname is specified, resolve the host from the "host:port" string. | ||
if (hostname != null) { | ||
return hostname.replace(/:\d+$/, '') | ||
} | ||
|
||
return host || DEFAULT_HOST | ||
} | ||
|
||
interface RequestAuth { | ||
username: string | ||
password: string | ||
|
@@ -109,22 +89,20 @@ function isRawIPv6Address(host: string): boolean { | |
return host.includes(':') && !host.startsWith('[') && !host.endsWith(']') | ||
} | ||
|
||
function getHostname(host: string, port?: number): string { | ||
const portString = typeof port !== 'undefined' ? `:${port}` : '' | ||
function getHostname(options: ResolvedRequestOptions): string | undefined { | ||
let host = options.hostname || options.host | ||
|
||
/** | ||
* @note As of Node >= 17, hosts (including "localhost") can resolve to IPv6 | ||
* addresses, so construct valid URL by surrounding the IPv6 host with brackets. | ||
*/ | ||
if (isRawIPv6Address(host)) { | ||
return `[${host}]${portString}` | ||
} | ||
if (host) { | ||
if (isRawIPv6Address(host)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kettanaito Maybe we should use https://nodejs.org/api/net.html#netisipv6input instead? |
||
host = `[${host}]` | ||
} | ||
|
||
if (typeof port === 'undefined') { | ||
return host | ||
// Check the presence of the port, and if it's present, | ||
// remove it from the host, returning a hostname. | ||
return new URL(`http://${host}`).hostname | ||
} | ||
|
||
return `${host}${portString}` | ||
return DEFAULT_HOSTNAME | ||
} | ||
|
||
/** | ||
|
@@ -146,13 +124,10 @@ export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL { | |
const protocol = getProtocolByRequestOptions(options) | ||
logger.info('protocol', protocol) | ||
|
||
const host = getHostByRequestOptions(options) | ||
logger.info('host', host) | ||
|
||
const port = getPortByRequestOptions(options) | ||
logger.info('port', port) | ||
|
||
const hostname = getHostname(host, port) | ||
const hostname = getHostname(options) | ||
logger.info('hostname', hostname) | ||
|
||
const path = options.path || DEFAULT_PATH | ||
|
@@ -166,7 +141,8 @@ export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL { | |
: '' | ||
logger.info('auth string:', authString) | ||
|
||
const url = new URL(`${protocol}//${hostname}${path}`) | ||
const portString = typeof port !== 'undefined' ? `:${port}` : '' | ||
const url = new URL(`${protocol}//${hostname}${portString}${path}`) | ||
url.username = credentials?.username || '' | ||
url.password = credentials?.password || '' | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { it, expect, beforeAll, afterAll } from 'vitest' | ||
import { httpGet } from '../../../helpers' | ||
import { ClientRequestInterceptor } from '../../../../src/interceptors/ClientRequest' | ||
import { DeferredPromise } from '@open-draft/deferred-promise' | ||
|
||
const interceptor = new ClientRequestInterceptor() | ||
|
||
beforeAll(() => { | ||
interceptor.apply() | ||
}) | ||
|
||
afterAll(() => { | ||
interceptor.dispose() | ||
}) | ||
|
||
it('supports requests with IPv6 request url', async () => { | ||
const url = 'http://[2607:f0d0:1002:51::4]:8080/' | ||
const listenerUrlPromise = new DeferredPromise<string>() | ||
|
||
interceptor.on('request', ({ request }) => { | ||
listenerUrlPromise.resolve(request.url) | ||
request.respondWith(new Response('test')) | ||
}); | ||
|
||
const { resBody } = await httpGet(url) | ||
const requestUrl = await listenerUrlPromise | ||
expect(resBody).toBe('test') | ||
expect(requestUrl).toBe(url) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be extremely careful around changes like this.
Yes, this is technically right—
hostname
does not include port. But in practice, I've seen a lot of libraries misuse Node.js. I fear this was one of those cases.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kettanaito, can you give an example? Because it's invalid and Node throws an error immediately:
And this is working:
![image](https://private-user-images.githubusercontent.com/11459632/324262230-60d4a921-5663-4859-b5ee-6d463028dfe6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjAyNDc1ODMsIm5iZiI6MTcyMDI0NzI4MywicGF0aCI6Ii8xMTQ1OTYzMi8zMjQyNjIyMzAtNjBkNGE5MjEtNTY2My00ODU5LWI1ZWUtNmQ0NjMwMjhkZmU2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA3MDYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNzA2VDA2MjgwM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTlmYWJiMThhNDNkNjAzOGFlZTdhOTQ3Njg3MWM4OWM1MGE1ZWU0MTBjY2U2Y2FiZmZlMmRiNjYzOGY3NzQ5YWUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.32tjiB-9EyW3iIQRgQW-C37nBBk88IWPGwn25zcSPoA)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point! Let's remove this old logic and if any issues arise, tackle them separately. I have no notion of supporting hacky tools, and if you use
http.request()
incorrectly, you are hacky.