Skip to content

Commit 474ecc1

Browse files
committed
Fix IPv6 hosts missing brackets in constructed URIs
REC1b2 treats an endpoint such as ::1 as an explicit hostname, but concatenating it into a URI produced https://::1:443/time, which URL parsers reject. Bracket IPv6 literals at the REST, WebSocket, and comet construction sites so the host identity stays unbracketed and only the URI is RFC 3986-compliant.
1 parent 42a6093 commit 474ecc1

6 files changed

Lines changed: 22 additions & 30 deletions

File tree

src/common/lib/client/baseclient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Logger, { LoggerOptions } from '../util/logger';
2-
import Defaults from '../util/defaults';
2+
import Defaults, { formatHostForUri } from '../util/defaults';
33
import Auth from './auth';
44
import { HttpPaginatedResponse, PaginatedResult } from './paginatedresource';
55
import ErrorInfo from '../types/errorinfo';
@@ -205,7 +205,7 @@ class BaseClient {
205205
}
206206

207207
baseUri(host: string) {
208-
return Defaults.getHttpScheme(this.options) + host + ':' + Defaults.getPort(this.options, false);
208+
return Defaults.getHttpScheme(this.options) + formatHostForUri(host) + ':' + Defaults.getPort(this.options, false);
209209
}
210210

211211
async stats(params?: RequestParams): Promise<PaginatedResult<Stats>> {

src/common/lib/transport/comettransport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ProtocolMessage, {
66
} from '../types/protocolmessage';
77
import Transport from './transport';
88
import Logger from '../util/logger';
9-
import Defaults from '../util/defaults';
9+
import Defaults, { formatHostForUri } from '../util/defaults';
1010
import ConnectionErrors from './connectionerrors';
1111
import Auth from '../client/auth';
1212
import ErrorInfo from '../types/errorinfo';
@@ -81,7 +81,7 @@ abstract class CometTransport extends Transport {
8181
const port = Defaults.getPort(options);
8282
const cometScheme = options.tls ? 'https://' : 'http://';
8383

84-
this.baseUri = cometScheme + host + ':' + port + '/comet/';
84+
this.baseUri = cometScheme + formatHostForUri(host) + ':' + port + '/comet/';
8585
const connectUri = this.baseUri + 'connect';
8686
Logger.logAction(this.logger, Logger.LOG_MINOR, 'CometTransport.connect()', 'uri: ' + connectUri);
8787
Utils.whenPromiseSettles(this.auth.getAuthParams(), (err: Error | null, authParams?: Record<string, any>) => {

src/common/lib/transport/websockettransport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Platform from 'common/platform';
22
import * as Utils from '../util/utils';
33
import Transport from './transport';
4-
import Defaults from '../util/defaults';
4+
import Defaults, { formatHostForUri } from '../util/defaults';
55
import Logger from '../util/logger';
66
import ProtocolMessage, {
77
serialize as serializeProtocolMessage,
@@ -52,7 +52,7 @@ class WebSocketTransport extends Transport {
5252
params = this.params,
5353
options = params.options;
5454
const wsScheme = options.tls ? 'wss://' : 'ws://';
55-
const wsUri = wsScheme + this.wsHost + ':' + Defaults.getPort(options) + '/';
55+
const wsUri = wsScheme + formatHostForUri(this.wsHost) + ':' + Defaults.getPort(options) + '/';
5656
Logger.logAction(this.logger, Logger.LOG_MINOR, 'WebSocketTransport.connect()', 'uri: ' + wsUri);
5757
Utils.whenPromiseSettles(
5858
this.auth.getAuthParams(),

src/common/lib/util/defaults.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ export function getHttpScheme(options: ClientOptions): string {
114114
return options.tls ? 'https://' : 'http://';
115115
}
116116

117+
/**
118+
* REC1b2 / RFC 3986 §3.2.2: an IPv6 literal used as a URI host must be enclosed in '[' and ']'.
119+
* Host identity is stored unbracketed; this is applied only when concatenating a URI.
120+
*/
121+
export function formatHostForUri(host: string): string {
122+
if (host.includes(':') && !host.startsWith('[')) {
123+
return '[' + host + ']';
124+
}
125+
return host;
126+
}
127+
117128
/**
118129
* REC1b2
119130
*/

test/uts/deviations.md

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,6 @@ These tests assert spec behavior but are skipped by default because they are kno
134134

135135
---
136136

137-
### fallback: REC1b2 - IPv6 endpoint address not bracketed
138-
139-
**Spec (REC1b2)**: IPv6 addresses should be supported as endpoint values.
140-
141-
**ably-js behavior**: URL construction produces `https://::1:443/time` instead of `https://[::1]:443/time`.
142-
143-
**Test**: `REC1b2 - endpoint as IPv6 address`.
144-
145-
**Issue**: [#2198](https://github.com/ably/ably-js/issues/2198)
146-
147-
---
148-
149137
### options_types: AO2 - authMethod default not stored
150138

151139
**Spec (AO2)**: `authMethod` should default to `'GET'` and be stored in auth options.

test/uts/rest/unit/fallback.test.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -816,8 +816,6 @@ describe('uts/rest/unit/fallback', function () {
816816

817817
// UTS: rest/unit/REC1b2/endpoint-ipv6-address-2
818818
it('REC1b2 - endpoint as IPv6 address', async function () {
819-
// DEVIATION: see deviations.md
820-
if (!process.env.RUN_DEVIATIONS) this.skip();
821819
const captured: any[] = [];
822820
const mock = new MockHttpClient({
823821
onConnectionAttempt: (conn) => conn.respond_with_success(),
@@ -828,17 +826,12 @@ describe('uts/rest/unit/fallback', function () {
828826
});
829827
installMockHttp(mock);
830828

831-
// Spec: endpoint '::1' should be treated as an explicit IPv6 hostname.
832-
// DEVIATION: ably-js constructs an invalid URI (no brackets around IPv6). See deviations.md.
833-
try {
834-
const client = new Ably.Rest({ key: 'app.key:secret', useBinaryProtocol: false, endpoint: '::1' });
835-
await client.time();
829+
const client = new Ably.Rest({ key: 'app.key:secret', useBinaryProtocol: false, endpoint: '::1' });
830+
await client.time();
836831

837-
expect(captured).to.have.length(1);
838-
expect(captured[0].url.hostname).to.satisfy((h: string) => h === '::1' || h === '[::1]');
839-
} catch (e) {
840-
expect.fail('IPv6 endpoint should work, but ably-js threw: ' + (e as Error).message);
841-
}
832+
expect(captured).to.have.length(1);
833+
expect(captured[0].url.hostname).to.satisfy((h: string) => h === '::1' || h === '[::1]');
834+
expect(captured[0].url.href).to.include('[::1]');
842835
});
843836

844837
// UTS: rest/unit/REC1b3/nonprod-routing-policy-0

0 commit comments

Comments
 (0)