Skip to content

Commit a28261d

Browse files
authored
fix(logger-plugin): support ipv6 host and handle undefined protocol/host
1 parent 5d28676 commit a28261d

4 files changed

Lines changed: 161 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- feat(hono): support for hono with createHonoProxyMiddleware
1919
- feat(ipv6): support literal IPv6 addresses in `target` and `forward` options (ie. "http://[::1]:8000")
2020
- chore(package.json): bump httpxy to ^0.5.1
21+
- fix(logger-plugin): support ipv6 host and handle undefined protocol/host
2122

2223
## [v3.0.5](https://github.com/chimurai/http-proxy-middleware/releases/tag/v3.0.5)
2324

src/plugins/default/logger-plugin.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { IncomingMessage } from 'node:http';
1+
import type { ClientRequest, IncomingMessage } from 'node:http';
22
import { URL } from 'node:url';
33

44
import { getLogger } from '../../logger.js';
55
import type { Plugin } from '../../types.js';
6+
import { createUrl } from '../../utils/create-url.js';
67
import { getPort } from '../../utils/logger-plugin.js';
78

89
type ExpressRequest = {
@@ -49,21 +50,12 @@ export const loggerPlugin: Plugin = (proxyServer, options) => {
4950

5051
try {
5152
const port = getPort(proxyRes.req?.agent?.sockets);
53+
const { protocol, host, path } = proxyRes.req as ClientRequest;
5254

53-
const obj = {
54-
protocol: proxyRes.req.protocol,
55-
host: proxyRes.req.host,
56-
pathname: proxyRes.req.path,
57-
} as URL;
58-
59-
target = new URL(`${obj.protocol}//${obj.host}${obj.pathname}`);
60-
61-
if (port) {
62-
target.port = port;
63-
}
64-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
55+
target = createUrl({ protocol, host, port, path });
6556
} catch (err) {
66-
// nock issue (https://github.com/chimurai/http-proxy-middleware/issues/1035)
57+
// should not error. keeping fallback just in case
58+
console.error('[HPM] Unexpected error while creating target URL', err);
6759
// fallback to old implementation (less correct - without port)
6860
target = new URL(options.target as URL);
6961
target.pathname = proxyRes.req.path;

src/utils/create-url.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { URL } from 'url';
2+
3+
type CreateUrlParams = {
4+
protocol?: string;
5+
host?: string;
6+
port?: string;
7+
path?: string;
8+
};
9+
10+
export function createUrl({ protocol, host, port, path }: CreateUrlParams): URL {
11+
// wrap IPv6 host in brackets
12+
const ipv6Host = host?.includes(':') ? `[${host}]` : host;
13+
14+
// use fallback values to create a valid URL (protocol: 'undefined:', host: '[::]')
15+
// nock v13 issue: protocol and host are undefined (https://github.com/chimurai/http-proxy-middleware/issues/1035)
16+
// nock v14+ seems to return protocol and host correctly
17+
const base = `${protocol || 'undefined:'}//${ipv6Host || '[::]'}`;
18+
const url = new URL(base);
19+
20+
if (port) {
21+
url.port = port;
22+
}
23+
24+
if (path) {
25+
url.pathname = path;
26+
}
27+
return url;
28+
}

test/unit/utils/create-url.spec.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { createUrl } from '../../../src/utils/create-url.js';
4+
5+
describe('createUrl()', () => {
6+
describe('happy paths', () => {
7+
it('should create a basic HTTP URL', () => {
8+
const url = createUrl({ protocol: 'http:', host: 'example.com', path: '/api' });
9+
expect(url.href).toBe('http://example.com/api');
10+
});
11+
12+
it('should create an HTTPS URL', () => {
13+
const url = createUrl({ protocol: 'https:', host: 'example.com', path: '/secure' });
14+
expect(url.href).toBe('https://example.com/secure');
15+
});
16+
17+
it('should include port when provided', () => {
18+
const url = createUrl({ protocol: 'http:', host: 'example.com', port: '3000', path: '/api' });
19+
expect(url.href).toBe('http://example.com:3000/api');
20+
});
21+
22+
it('should default pathname to / when path is omitted', () => {
23+
const url = createUrl({ protocol: 'http:', host: 'example.com' });
24+
expect(url.href).toBe('http://example.com/');
25+
});
26+
27+
it('should handle an IPv4 host', () => {
28+
const url = createUrl({
29+
protocol: 'http:',
30+
host: '127.0.0.1',
31+
port: '8080',
32+
path: '/health',
33+
});
34+
expect(url.href).toBe('http://127.0.0.1:8080/health');
35+
});
36+
37+
it('should wrap an IPv6 localhost address in brackets', () => {
38+
const url = createUrl({ protocol: 'http:', host: '::1', port: '8080', path: '/' });
39+
expect(url.href).toBe('http://[::1]:8080/');
40+
});
41+
42+
it('should wrap the IPv6 unspecified address in brackets', () => {
43+
const url = createUrl({ protocol: 'http:', host: '::', path: '/status' });
44+
expect(url.href).toBe('http://[::]/status');
45+
});
46+
47+
it('should double-bracket an already-bracketed IPv6 host (pass-through behavior)', () => {
48+
// '[::1]' contains ':' so the function wraps it again → '[[::1]]', which is an invalid URL
49+
expect(() => createUrl({ protocol: 'http:', host: '[::1]', port: '9000' })).toThrow();
50+
});
51+
52+
it('should handle a root path explicitly', () => {
53+
const url = createUrl({ protocol: 'http:', host: 'localhost', path: '/' });
54+
expect(url.href).toBe('http://localhost/');
55+
});
56+
57+
it('should handle a deep nested path', () => {
58+
const url = createUrl({ protocol: 'http:', host: 'api.example.com', path: '/v1/users/42' });
59+
expect(url.href).toBe('http://api.example.com/v1/users/42');
60+
});
61+
});
62+
63+
describe('edge cases — missing / undefined inputs', () => {
64+
it('should fall back gracefully when protocol is undefined', () => {
65+
const url = createUrl({ host: 'example.com', path: '/api' });
66+
expect(url.href).toBe('undefined://example.com/api');
67+
});
68+
69+
it('should fall back gracefully when host is undefined', () => {
70+
const url = createUrl({ protocol: 'http:', path: '/api' });
71+
expect(url.href).toBe('http://[::]/api');
72+
});
73+
74+
it('should fall back gracefully when both protocol and host are undefined (nock v13 scenario)', () => {
75+
const url = createUrl({ path: '/api' });
76+
expect(url.href).toBe('undefined://[::]/api');
77+
});
78+
79+
it('should not set port when port is omitted', () => {
80+
const url = createUrl({ protocol: 'http:', host: 'example.com', path: '/no-port' });
81+
expect(url.href).toBe('http://example.com/no-port');
82+
});
83+
84+
it('should not set pathname when path is omitted', () => {
85+
const url = createUrl({ protocol: 'http:', host: 'example.com' });
86+
expect(url.href).toBe('http://example.com/');
87+
});
88+
});
89+
90+
describe('edge cases — empty string inputs', () => {
91+
it('should use fallback protocol when protocol is an empty string', () => {
92+
// empty string is falsy: fallback 'undefined:' is used
93+
const url = createUrl({ protocol: '', host: 'example.com', path: '/api' });
94+
expect(url.href).toBe('undefined://example.com/api');
95+
});
96+
97+
it('should use fallback host when host is an empty string', () => {
98+
// empty string host → ipv6Host is '' (falsy) → fallback '[::]' used
99+
const url = createUrl({ protocol: 'http:', host: '', path: '/api' });
100+
expect(url.href).toBe('http://[::]/api');
101+
});
102+
103+
it('should not set pathname when path is an empty string', () => {
104+
// empty string is falsy, so the `if (path)` branch is skipped
105+
const url = createUrl({ protocol: 'http:', host: 'example.com', path: '' });
106+
expect(url.href).toBe('http://example.com/');
107+
});
108+
109+
it('should not set port when port is an empty string', () => {
110+
const url = createUrl({ protocol: 'http:', host: 'example.com', port: '', path: '/api' });
111+
expect(url.href).toBe('http://example.com/api');
112+
});
113+
});
114+
115+
describe('edge cases — numeric port (runtime flexibility)', () => {
116+
it('should accept a numeric port value at runtime', () => {
117+
// type is string but runtime numeric values can occur; `url.port` coerces via assignment
118+
const url = createUrl({
119+
protocol: 'http:',
120+
host: 'example.com',
121+
port: 8080 as unknown as string,
122+
});
123+
expect(url.href).toBe('http://example.com:8080/');
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)