You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Imposter.uri() always reports an http:// URI, even for an imposter created with "protocol": "https". ImposterImpl.uri() builds it with HostAuthority.httpUri(host, port), which hard-codes the scheme (HostAuthority.java:27-28). The hostResolver fallback also seems to assume http, though I haven't checked that path fully.
Effect: a caller that trusts uri() sends plain HTTP to a TLS listener, and the request fails. zio-bdd sets MockSpace.baseUri from this value, so its native https escape hatch was unreachable. It now rewrites the scheme itself from the protocol it created the imposter with. That workaround should go once this is fixed.
Proposed fix: derive the scheme from the imposter's protocol when building the URI. For example, add HostAuthority.uri(scheme, host, port) and pass https when the definition's protocol is https. Apply the same to the hostResolver path.
Repro: create an imposter from {"protocol":"https","cert":"<pem>","key":"<pem>","stubs":[]}, then call uri(). It returns http://localhost:<port>, where https://localhost:<port> is expected.
Should-question: settled. This is a correctness bug in an existing accessor, not new surface.
Triage — confirmed; design decided: the imposter's protocol is the scheme, and the resolver is told it
SDK refs are rift-java master at 88b8849 (0.3.2-SNAPSHOT); engine refs are ~/Projects/rift at 0.18.0.
Desirability: build — defect in an existing accessor. uri() is documented as where to send requests; for an https imposter it names a scheme the listener refuses. Recommended model: Opus — small change, but with one real subtlety (the testcontainers gateway, below) and a public-seam change on ConnectOptions.
Root cause
The scheme is hard-coded in two independent places, and ImposterImpl never learns the protocol at all.
Resolver path — every resolver the SDK ships bakes in a scheme unrelated to the imposter, because the seam is IntFunction<URI> and receives only a port:
ConnectOptions.defaultHostResolver (ConnectOptions.java:102-107) copies the admin URI's scheme. That is the wrong object: the admin listener's scheme says nothing about an imposter's. (It also means an https admin URI would report https for a plain-http imposter.)
RiftImpl.embedded (RiftImpl.java:121) and RiftContainer.client() direct mode (RiftContainer.java:240) use httpUri.
ImposterImpl carries only port and boundHost. The protocol is available in every place the host is already read from — the posted JSON on create (RiftImpl.create(JsonValue)), the replayable list on a local lookup — and additionally in the single GET, which the connected-engine imposter(port) path currently discards (RiftImpl.java:341-342 passes JsonObject.of() to imposterAt).
Engine facts (checked, not inferred): protocol is a required String on the imposter config (crates/rift-mock-core/src/imposter/types.rs:1435) and is always serialized, so every GET and list item carries it; the engine accepts only "http" and "https" (manager.rs:813, 1587), so scheme == protocol with no mapping table. The SDK already defaults an absent protocol to http (ImposterDefinition.DEFAULT_PROTOCOL), matching the engine.
In-repo symptom:ImposterClientAuthIT.java:91 hard-codes "https://127.0.0.1:" + imp.port() instead of imp.uri() — the live test had to work around this bug the same way zio-bdd did.
The subtlety: the resolver's URI is not always the imposter's listener
The proposed fix in the issue ("derive the scheme from the protocol … apply the same to the hostResolver path") cannot be applied as a blanket scheme override on the resolver's result. RiftContainer.withGateway() resolves to http://<host>:<mappedAdminPort>/__rift/<port> (RiftContainer.java:239). There the SUT speaks plain HTTP to the admin gateway, which dispatches to the imposter in-process (crates/rift-http-proxy/src/gateway.rs:35-40); forcing https onto that URI would break gateway mode for every https imposter. So the resolver must be told the protocol and decide for itself whether it applies. A resolver that cannot see the protocol cannot produce a correct URI — the IntFunction<URI> type is part of the bug.
Design
HostAuthority — add public static URI uri(String scheme, String host, int port); httpUri(host, port) becomes uri("http", host, port) and stays (admin URIs, RiftProcess, InterceptImpl are genuinely http).
New seam type — io.github.achirdlabs.rift.HostResolver:
@FunctionalInterfacepublicinterfaceHostResolver {
/** The address the SUT should use for the imposter bound on {@code port}, whose engine protocol is {@code protocol} ("http" or "https"). */URIresolve(Stringprotocol, intport);
}
ConnectOptions.hostResolver() returns HostResolver (was IntFunction<URI>; the only caller in the tree is ImposterImpl).
ConnectOptions.Builder.hostResolver(HostResolver) — the new form.
ConnectOptions.Builder.hostResolver(IntFunction<URI>) — kept, adapted as (protocol, port) -> legacy.apply(port). Its URI is used verbatim, scheme included; Javadoc says so and points at the two-arg form. Overload resolution is unambiguous: the lambdas differ in arity. (Not deprecated: a resolver that targets a TLS-terminating hop legitimately wants to own the scheme.)
defaultHostResolver(adminUri) → (protocol, port) -> HostAuthority.uri(protocol, adminUri.getHost(), port). getHost() keeps IPv6 brackets and bracketed() leaves a bracketed host alone, so no behaviour change on the host.
RiftContainer.client() direct mode → (protocol, port) -> HostAuthority.uri(protocol, getHost(), getMappedPort(port)); gateway mode → (protocol, port) -> URI.create(admin + "/__rift/" + port) with a comment: the gateway is reached at the admin listener's scheme whatever the imposter speaks.
ImposterImpl — gains private final String protocol, set at construction like boundHost:
Constructor (port, transport, options, Optional<String> boundHost, String protocol); the 3-arg convenience constructor (used by five unit tests) passes Optional.empty(), ImposterDefinition.DEFAULT_PROTOCOL.
Caching is safe: protocol and host are fixed for an imposter's lifetime on a port (the engine has no per-imposter PUT; replaceAll/delete+recreate already leaves an old handle's host stale, and protocol joins it).
RiftImpl.imposterAt(port, definition) — read protocol from the JSON alongside host, defaulting to ImposterDefinition.DEFAULT_PROTOCOL when absent (raw-JSON creates may omit it; the engine defaults to http). Unlike host, protocol is read for connected engines too — it is an attribute of the imposter, not an address. The connected imposter(port) path passes the GET body to imposterAt instead of JsonObject.of().
Out of scope, noted: InterceptImpl.uri() is an HTTP proxy address and is correctly http:// even when the intercept does TLS MITM via CONNECT.
ImposterUriHostTest (existing fake transport; note its createImposter reply is {"port":4545} with no protocol, so these prove create reads the protocol from the posted JSON):
https imposter with a concrete host on spawned and embedded → https://127.0.0.2:4545, https://[::1]:4545;
https imposter with no host / wildcard host → resolver path → https://127.0.0.1:4545 (spawned default resolver) and https://[::1]:4545 (embedded with adminHost("::1"));
lookups: list item {"port":4545,"host":"::1","protocol":"https"} → https://[::1]:4545; an item without protocol → http://.
default resolver, https imposter created → https://<adminHost>:4545; rift.imposter(4545) with a GET body {"port":4545,"protocol":"https"} → https://… (the discarded-body fix);
legacy IntFunction<URI> resolver returning http://sut-host:… for an https imposter → URI used verbatim (existing hostResolverControlsImposterUri keeps passing);
two-arg resolver receives "https" and can honour it.
Live, ImposterClientAuthIT: replace the hard-coded "https://127.0.0.1:" + imp.port() with imp.uri() — spawned engine, absent host, so this exercises the default resolver with https. Add one case with .host("127.0.0.1").https(...) to exercise the bound-host path live.
testcontainers (optional, IT only since client() needs a started container): direct mode reports https:// for an https imposter; gateway mode still reports http://…/__rift/<port> for the same imposter.
ConnectOptions.Builder.hostResolver(..) Javadoc for both overloads (verbatim-scheme contract of the legacy form).
docs/testcontainers.md (lines ~35 and ~50): direct-mode comment gains the scheme rule; gateway section states the URI is always the admin listener's scheme.
docs/design/sdk-api.md:166-167 (hostResolver type and contract) and :312-314 (uri() rule).
Order
Independent; no engine change needed. After merge, ping EtaCassiopeia/zio-bdd#343 so the scheme-rewrite workaround from PR #345 can be removed.
Found by: EtaCassiopeia/zio-bdd#343 (PR EtaCassiopeia/zio-bdd#345), rift-java 0.3.1.
Imposter.uri()always reports anhttp://URI, even for an imposter created with"protocol": "https".ImposterImpl.uri()builds it withHostAuthority.httpUri(host, port), which hard-codes the scheme (HostAuthority.java:27-28). ThehostResolverfallback also seems to assume http, though I haven't checked that path fully.Effect: a caller that trusts
uri()sends plain HTTP to a TLS listener, and the request fails. zio-bdd setsMockSpace.baseUrifrom this value, so its native https escape hatch was unreachable. It now rewrites the scheme itself from the protocol it created the imposter with. That workaround should go once this is fixed.Proposed fix: derive the scheme from the imposter's protocol when building the URI. For example, add
HostAuthority.uri(scheme, host, port)and passhttpswhen the definition's protocol ishttps. Apply the same to thehostResolverpath.Repro: create an imposter from
{"protocol":"https","cert":"<pem>","key":"<pem>","stubs":[]}, then calluri(). It returnshttp://localhost:<port>, wherehttps://localhost:<port>is expected.Should-question: settled. This is a correctness bug in an existing accessor, not new surface.
Triage — confirmed; design decided: the imposter's protocol is the scheme, and the resolver is told it
SDK refs are rift-java
masterat88b8849(0.3.2-SNAPSHOT); engine refs are~/Projects/riftat 0.18.0.Desirability: build — defect in an existing accessor.
uri()is documented as where to send requests; for an https imposter it names a scheme the listener refuses.Recommended model: Opus — small change, but with one real subtlety (the testcontainers gateway, below) and a public-seam change on
ConnectOptions.Root cause
The scheme is hard-coded in two independent places, and
ImposterImplnever learns the protocol at all.ImposterImpl.uri()(rift-java-core/.../ImposterImpl.java:73) callsHostAuthority.httpUri(host, port), which is literally"http://" + …(HostAuthority.java:27-28). This is the path a spawned/embedded engine takes for an imposter with a concretehost(fix(core): Imposter.uri() reports the admin host, not the host the imposter is bound to #246).IntFunction<URI>and receives only a port:ConnectOptions.defaultHostResolver(ConnectOptions.java:102-107) copies the admin URI's scheme. That is the wrong object: the admin listener's scheme says nothing about an imposter's. (It also means an https admin URI would reporthttpsfor a plain-http imposter.)RiftImpl.embedded(RiftImpl.java:121) andRiftContainer.client()direct mode (RiftContainer.java:240) usehttpUri.ImposterImplcarries onlyportandboundHost. The protocol is available in every place the host is already read from — the posted JSON oncreate(RiftImpl.create(JsonValue)), the replayable list on a local lookup — and additionally in the single GET, which the connected-engineimposter(port)path currently discards (RiftImpl.java:341-342passesJsonObject.of()toimposterAt).Engine facts (checked, not inferred):
protocolis a requiredStringon the imposter config (crates/rift-mock-core/src/imposter/types.rs:1435) and is always serialized, so every GET and list item carries it; the engine accepts only"http"and"https"(manager.rs:813,1587), soscheme == protocolwith no mapping table. The SDK already defaults an absentprotocoltohttp(ImposterDefinition.DEFAULT_PROTOCOL), matching the engine.In-repo symptom:
ImposterClientAuthIT.java:91hard-codes"https://127.0.0.1:" + imp.port()instead ofimp.uri()— the live test had to work around this bug the same way zio-bdd did.The subtlety: the resolver's URI is not always the imposter's listener
The proposed fix in the issue ("derive the scheme from the protocol … apply the same to the
hostResolverpath") cannot be applied as a blanket scheme override on the resolver's result.RiftContainer.withGateway()resolves tohttp://<host>:<mappedAdminPort>/__rift/<port>(RiftContainer.java:239). There the SUT speaks plain HTTP to the admin gateway, which dispatches to the imposter in-process (crates/rift-http-proxy/src/gateway.rs:35-40); forcinghttpsonto that URI would break gateway mode for every https imposter. So the resolver must be told the protocol and decide for itself whether it applies. A resolver that cannot see the protocol cannot produce a correct URI — theIntFunction<URI>type is part of the bug.Design
HostAuthority— addpublic static URI uri(String scheme, String host, int port);httpUri(host, port)becomesuri("http", host, port)and stays (admin URIs,RiftProcess,InterceptImplare genuinely http).New seam type —
io.github.achirdlabs.rift.HostResolver:ConnectOptions.hostResolver()returnsHostResolver(wasIntFunction<URI>; the only caller in the tree isImposterImpl).ConnectOptions.Builder.hostResolver(HostResolver)— the new form.ConnectOptions.Builder.hostResolver(IntFunction<URI>)— kept, adapted as(protocol, port) -> legacy.apply(port). Its URI is used verbatim, scheme included; Javadoc says so and points at the two-arg form. Overload resolution is unambiguous: the lambdas differ in arity. (Not deprecated: a resolver that targets a TLS-terminating hop legitimately wants to own the scheme.)defaultHostResolver(adminUri)→(protocol, port) -> HostAuthority.uri(protocol, adminUri.getHost(), port).getHost()keeps IPv6 brackets andbracketed()leaves a bracketed host alone, so no behaviour change on the host.RiftImpl.embedded→(protocol, port) -> HostAuthority.uri(protocol, options.adminHost(), port).RiftContainer.client()direct mode →(protocol, port) -> HostAuthority.uri(protocol, getHost(), getMappedPort(port)); gateway mode →(protocol, port) -> URI.create(admin + "/__rift/" + port)with a comment: the gateway is reached at the admin listener's scheme whatever the imposter speaks.ImposterImpl— gainsprivate final String protocol, set at construction likeboundHost:uri()=boundHost.map(h -> HostAuthority.uri(protocol, h, port)).orElseGet(() -> options.hostResolver().resolve(protocol, port)).(port, transport, options, Optional<String> boundHost, String protocol); the 3-arg convenience constructor (used by five unit tests) passesOptional.empty(), ImposterDefinition.DEFAULT_PROTOCOL.replaceAll/delete+recreate already leaves an old handle's host stale, and protocol joins it).RiftImpl.imposterAt(port, definition)— readprotocolfrom the JSON alongsidehost, defaulting toImposterDefinition.DEFAULT_PROTOCOLwhen absent (raw-JSON creates may omit it; the engine defaults to http). Unlikehost, protocol is read for connected engines too — it is an attribute of the imposter, not an address. The connectedimposter(port)path passes the GET body toimposterAtinstead ofJsonObject.of().Out of scope, noted:
InterceptImpl.uri()is an HTTP proxy address and is correctlyhttp://even when the intercept does TLS MITM via CONNECT.Tests
HostAuthorityTest:uri("https", "::1", 4545)→https://[::1]:4545;uri("http", "127.0.0.1", 2525)equalshttpUri(...).ImposterUriHostTest(existing fake transport; note itscreateImposterreply is{"port":4545}with no protocol, so these provecreatereads the protocol from the posted JSON):https://127.0.0.2:4545,https://[::1]:4545;https://127.0.0.1:4545(spawned default resolver) andhttps://[::1]:4545(embedded withadminHost("::1"));{"port":4545,"host":"::1","protocol":"https"}→https://[::1]:4545; an item withoutprotocol→http://.RemoteTransportGateTest/RemoteTransportCoverageTest(connected engine,FakeAdminServer):https://<adminHost>:4545;rift.imposter(4545)with a GET body{"port":4545,"protocol":"https"}→https://…(the discarded-body fix);IntFunction<URI>resolver returninghttp://sut-host:…for an https imposter → URI used verbatim (existinghostResolverControlsImposterUrikeeps passing);"https"and can honour it.ImposterClientAuthIT: replace the hard-coded"https://127.0.0.1:" + imp.port()withimp.uri()— spawned engine, absent host, so this exercises the default resolver with https. Add one case with.host("127.0.0.1").https(...)to exercise the bound-host path live.client()needs a started container): direct mode reportshttps://for an https imposter; gateway mode still reportshttp://…/__rift/<port>for the same imposter.Docs
Imposter.uri()Javadoc: the scheme is the imposter'sprotocol; the host rule from fix(core): Imposter.uri() reports the admin host, not the host the imposter is bound to #246 is unchanged.ConnectOptions.Builder.hostResolver(..)Javadoc for both overloads (verbatim-scheme contract of the legacy form).docs/testcontainers.md(lines ~35 and ~50): direct-mode comment gains the scheme rule; gateway section states the URI is always the admin listener's scheme.docs/design/sdk-api.md:166-167(hostResolvertype and contract) and:312-314(uri()rule).Order
Independent; no engine change needed. After merge, ping EtaCassiopeia/zio-bdd#343 so the scheme-rewrite workaround from PR #345 can be removed.