authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:49:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:49:21-07:00
log0e718855b899b8535ee84e3721f4c5a0de0934cb
treed52971368b460d36dd2133033c0a2017074b8d4c
parent731b1a67acb454da7a52491ab08db39c57036699
signature Signed by PGP key B5690EEEBB952194

[codex] Structure primary environment target failures (#3413)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 240 insertions(+), 27 deletions(-)

apps/web/src/environments/primary/bootstrap.test.ts+76
......@@ -4,6 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
44
55import {
66 getPrimaryKnownEnvironment,
7 isDesktopEnvironmentBootstrapIncompleteError,
8 isPrimaryEnvironmentProtocolUnsupportedError,
9 isPrimaryEnvironmentUrlInvalidError,
10 readPrimaryEnvironmentTarget,
711 resolvePrimaryEnvironmentHttpUrl,
812 resolveInitialPrimaryEnvironmentDescriptor,
913 resetPrimaryEnvironmentDescriptorForTests,
......@@ -43,6 +47,15 @@ function installTestBrowser(url: string) {
4347 });
4448}
4549
50function captureThrown(run: () => unknown): unknown {
51 try {
52 run();
53 } catch (error) {
54 return error;
55 }
56 throw new Error("Expected the operation to throw.");
57}
58
4659describe("environmentBootstrap", () => {
4760 beforeEach(() => {
4861 vi.restoreAllMocks();
......@@ -175,4 +188,67 @@ describe("environmentBootstrap", () => {
175188 "http://127.0.0.1:5733/.well-known/t3/environment",
176189 );
177190 });
191
192 it("retains the URL parser cause without exposing the configured URL in its message", () => {
193 vi.stubEnv("VITE_HTTP_URL", "http://[");
194
195 const error = captureThrown(readPrimaryEnvironmentTarget);
196
197 expect(isPrimaryEnvironmentUrlInvalidError(error)).toBe(true);
198 if (!isPrimaryEnvironmentUrlInvalidError(error)) {
199 throw new Error("Expected a structured primary environment URL error.");
200 }
201 expect(error).toMatchObject({
202 source: "configured",
203 urlKind: "http-base-url",
204 message: "Could not parse http-base-url for the configured primary environment target.",
205 });
206 expect(error.cause).toBeInstanceOf(TypeError);
207 expect(error.message).not.toContain("http://[");
208 });
209
210 it("describes which desktop bootstrap endpoint is missing", () => {
211 vi.stubGlobal("window", {
212 location: new URL("http://127.0.0.1:5733/"),
213 history: { replaceState: vi.fn() },
214 desktopBridge: {
215 getLocalEnvironmentBootstrap: () => ({
216 label: "Local environment",
217 httpBaseUrl: "http://127.0.0.1:3773",
218 bootstrapToken: "desktop-bootstrap-token",
219 }),
220 },
221 });
222
223 const error = captureThrown(readPrimaryEnvironmentTarget);
224
225 expect(isDesktopEnvironmentBootstrapIncompleteError(error)).toBe(true);
226 if (!isDesktopEnvironmentBootstrapIncompleteError(error)) {
227 throw new Error("Expected a structured desktop bootstrap error.");
228 }
229 expect(error).toMatchObject({
230 hasHttpBaseUrl: true,
231 hasWsBaseUrl: false,
232 message: "Desktop bootstrap is missing wsBaseUrl for the local environment.",
233 });
234 });
235
236 it("preserves an unsupported window-origin protocol", () => {
237 vi.stubGlobal("window", {
238 location: { origin: "file:///tmp/t3code/" },
239 history: { replaceState: vi.fn() },
240 });
241
242 const error = captureThrown(readPrimaryEnvironmentTarget);
243
244 expect(isPrimaryEnvironmentProtocolUnsupportedError(error)).toBe(true);
245 if (!isPrimaryEnvironmentProtocolUnsupportedError(error)) {
246 throw new Error("Expected a structured primary environment protocol error.");
247 }
248 expect(error).toMatchObject({
249 source: "window-origin",
250 protocol: "file:",
251 message: "The window-origin primary environment target uses unsupported protocol file:.",
252 });
253 });
178254});
apps/web/src/environments/primary/index.ts+7
......@@ -38,7 +38,14 @@ export { refreshPrimarySessionState, usePrimarySessionState } from "./sessionSta
3838export { PrimaryEnvironmentHttpClient } from "./httpClient";
3939
4040export {
41 DesktopEnvironmentBootstrapIncompleteError,
42 isDesktopEnvironmentBootstrapIncompleteError,
43 isPrimaryEnvironmentProtocolUnsupportedError,
44 isPrimaryEnvironmentUrlInvalidError,
45 PrimaryEnvironmentProtocolUnsupportedError,
46 PrimaryEnvironmentUrlInvalidError,
4147 readPrimaryEnvironmentTarget,
4248 resolvePrimaryEnvironmentHttpUrl,
4349 isLoopbackHostname,
50 type PrimaryEnvironmentTarget,
4451} from "./target";
apps/web/src/environments/primary/target.ts+157-27
......@@ -1,7 +1,72 @@
11import type { DesktopEnvironmentBootstrap } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3
4const PrimaryEnvironmentTargetSource = Schema.Literals([
5 "configured",
6 "window-origin",
7 "desktop-managed",
8]);
9type PrimaryEnvironmentTargetSource = typeof PrimaryEnvironmentTargetSource.Type;
10
11const PrimaryEnvironmentUrlKind = Schema.Literals([
12 "http-base-url",
13 "websocket-base-url",
14 "development-server-url",
15 "window-location-url",
16]);
17type PrimaryEnvironmentUrlKind = typeof PrimaryEnvironmentUrlKind.Type;
18
19export class PrimaryEnvironmentUrlInvalidError extends Schema.TaggedErrorClass<PrimaryEnvironmentUrlInvalidError>()(
20 "PrimaryEnvironmentUrlInvalidError",
21 {
22 source: PrimaryEnvironmentTargetSource,
23 urlKind: PrimaryEnvironmentUrlKind,
24 cause: Schema.Defect(),
25 },
26) {
27 override get message(): string {
28 return `Could not parse ${this.urlKind} for the ${this.source} primary environment target.`;
29 }
30}
31
32export class PrimaryEnvironmentProtocolUnsupportedError extends Schema.TaggedErrorClass<PrimaryEnvironmentProtocolUnsupportedError>()(
33 "PrimaryEnvironmentProtocolUnsupportedError",
34 {
35 source: PrimaryEnvironmentTargetSource,
36 protocol: Schema.String,
37 },
38) {
39 override get message(): string {
40 return `The ${this.source} primary environment target uses unsupported protocol ${this.protocol}.`;
41 }
42}
43
44export class DesktopEnvironmentBootstrapIncompleteError extends Schema.TaggedErrorClass<DesktopEnvironmentBootstrapIncompleteError>()(
45 "DesktopEnvironmentBootstrapIncompleteError",
46 {
47 hasHttpBaseUrl: Schema.Boolean,
48 hasWsBaseUrl: Schema.Boolean,
49 },
50) {
51 override get message(): string {
52 const missing = [
53 ...(this.hasHttpBaseUrl ? [] : ["httpBaseUrl"]),
54 ...(this.hasWsBaseUrl ? [] : ["wsBaseUrl"]),
55 ];
56 return `Desktop bootstrap is missing ${missing.join(" and ")} for the local environment.`;
57 }
58}
59
60export const isPrimaryEnvironmentUrlInvalidError = Schema.is(PrimaryEnvironmentUrlInvalidError);
61export const isPrimaryEnvironmentProtocolUnsupportedError = Schema.is(
62 PrimaryEnvironmentProtocolUnsupportedError,
63);
64export const isDesktopEnvironmentBootstrapIncompleteError = Schema.is(
65 DesktopEnvironmentBootstrapIncompleteError,
66);
267
368export interface PrimaryEnvironmentTarget {
4 readonly source: "configured" | "window-origin" | "desktop-managed";
69 readonly source: PrimaryEnvironmentTargetSource;
570 readonly target: {
671 readonly httpBaseUrl: string;
772 readonly wsBaseUrl: string;
......@@ -14,15 +79,49 @@ function getDesktopLocalEnvironmentBootstrap(): DesktopEnvironmentBootstrap | nu
1479 return window.desktopBridge?.getLocalEnvironmentBootstrap() ?? null;
1580}
1681
17function normalizeBaseUrl(rawValue: string): string {
18 return new URL(rawValue, window.location.origin).toString();
82function parseTargetUrl(input: {
83 readonly rawValue: string;
84 readonly baseUrl?: string;
85 readonly source: PrimaryEnvironmentTargetSource;
86 readonly urlKind: PrimaryEnvironmentUrlKind;
87}): URL {
88 try {
89 return input.baseUrl === undefined
90 ? new URL(input.rawValue)
91 : new URL(input.rawValue, input.baseUrl);
92 } catch (cause) {
93 throw new PrimaryEnvironmentUrlInvalidError({
94 source: input.source,
95 urlKind: input.urlKind,
96 cause,
97 });
98 }
99}
100
101function normalizeBaseUrl(
102 rawValue: string,
103 source: PrimaryEnvironmentTargetSource,
104 urlKind: PrimaryEnvironmentUrlKind,
105): string {
106 return parseTargetUrl({
107 rawValue,
108 baseUrl: window.location.origin,
109 source,
110 urlKind,
111 }).toString();
19112}
20113
21114function swapBaseUrlProtocol(
22115 rawValue: string,
23116 nextProtocol: "http:" | "https:" | "ws:" | "wss:",
117 urlKind: PrimaryEnvironmentUrlKind,
24118): string {
25 const url = new URL(normalizeBaseUrl(rawValue));
119 const url = parseTargetUrl({
120 rawValue,
121 baseUrl: window.location.origin,
122 source: "configured",
123 urlKind,
124 });
26125 url.protocol = nextProtocol;
27126 return url.toString();
28127}
......@@ -38,15 +137,29 @@ export function isLoopbackHostname(hostname: string): boolean {
38137 return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
39138}
40139
41function resolveHttpRequestBaseUrl(httpBaseUrl: string): string {
140function resolveHttpRequestBaseUrl(primaryTarget: PrimaryEnvironmentTarget): string {
141 const httpBaseUrl = primaryTarget.target.httpBaseUrl;
42142 const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim();
43143 if (!configuredDevServerUrl) {
44144 return httpBaseUrl;
45145 }
46146
47 const currentUrl = new URL(window.location.href);
48 const targetUrl = new URL(httpBaseUrl);
49 const devServerUrl = new URL(configuredDevServerUrl, currentUrl.origin);
147 const currentUrl = parseTargetUrl({
148 rawValue: window.location.href,
149 source: "window-origin",
150 urlKind: "window-location-url",
151 });
152 const targetUrl = parseTargetUrl({
153 rawValue: httpBaseUrl,
154 source: primaryTarget.source,
155 urlKind: "http-base-url",
156 });
157 const devServerUrl = parseTargetUrl({
158 rawValue: configuredDevServerUrl,
159 baseUrl: currentUrl.origin,
160 source: "configured",
161 urlKind: "development-server-url",
162 });
50163
51164 const isCurrentOriginDevServer =
52165 (currentUrl.protocol === "http:" || currentUrl.protocol === "https:") &&
......@@ -75,32 +188,39 @@ function resolveConfiguredPrimaryTarget(): PrimaryEnvironmentTarget | null {
75188 const resolvedHttpBaseUrl =
76189 configuredHttpBaseUrl ??
77190 (configuredWsBaseUrl?.startsWith("wss:")
78 ? swapBaseUrlProtocol(configuredWsBaseUrl, "https:")
79 : swapBaseUrlProtocol(configuredWsBaseUrl!, "http:"));
191 ? swapBaseUrlProtocol(configuredWsBaseUrl, "https:", "websocket-base-url")
192 : swapBaseUrlProtocol(configuredWsBaseUrl!, "http:", "websocket-base-url"));
80193 const resolvedWsBaseUrl =
81194 configuredWsBaseUrl ??
82195 (configuredHttpBaseUrl?.startsWith("https:")
83 ? swapBaseUrlProtocol(configuredHttpBaseUrl, "wss:")
84 : swapBaseUrlProtocol(configuredHttpBaseUrl!, "ws:"));
196 ? swapBaseUrlProtocol(configuredHttpBaseUrl, "wss:", "http-base-url")
197 : swapBaseUrlProtocol(configuredHttpBaseUrl!, "ws:", "http-base-url"));
85198
86199 return {
87200 source: "configured",
88201 target: {
89 httpBaseUrl: normalizeBaseUrl(resolvedHttpBaseUrl),
90 wsBaseUrl: normalizeBaseUrl(resolvedWsBaseUrl),
202 httpBaseUrl: normalizeBaseUrl(resolvedHttpBaseUrl, "configured", "http-base-url"),
203 wsBaseUrl: normalizeBaseUrl(resolvedWsBaseUrl, "configured", "websocket-base-url"),
91204 },
92205 };
93206}
94207
95208function resolveWindowOriginPrimaryTarget(): PrimaryEnvironmentTarget {
96 const httpBaseUrl = normalizeBaseUrl(window.location.origin);
97 const url = new URL(httpBaseUrl);
209 const url = parseTargetUrl({
210 rawValue: window.location.origin,
211 source: "window-origin",
212 urlKind: "http-base-url",
213 });
214 const httpBaseUrl = url.toString();
98215 if (url.protocol === "http:") {
99216 url.protocol = "ws:";
100217 } else if (url.protocol === "https:") {
101218 url.protocol = "wss:";
102219 } else {
103 throw new Error(`Unsupported HTTP base URL protocol: ${url.protocol}`);
220 throw new PrimaryEnvironmentProtocolUnsupportedError({
221 source: "window-origin",
222 protocol: url.protocol,
223 });
104224 }
105225 return {
106226 source: "window-origin",
......@@ -120,16 +240,25 @@ function resolveDesktopPrimaryTarget(): PrimaryEnvironmentTarget | null {
120240 return null;
121241 }
122242 if (!desktopBootstrap.httpBaseUrl || !desktopBootstrap.wsBaseUrl) {
123 throw new Error(
124 "Desktop bootstrap must provide both httpBaseUrl and wsBaseUrl for the local environment.",
125 );
243 throw new DesktopEnvironmentBootstrapIncompleteError({
244 hasHttpBaseUrl: Boolean(desktopBootstrap.httpBaseUrl),
245 hasWsBaseUrl: Boolean(desktopBootstrap.wsBaseUrl),
246 });
126247 }
127248
128249 return {
129250 source: "desktop-managed",
130251 target: {
131 httpBaseUrl: normalizeBaseUrl(desktopBootstrap.httpBaseUrl),
132 wsBaseUrl: normalizeBaseUrl(desktopBootstrap.wsBaseUrl),
252 httpBaseUrl: normalizeBaseUrl(
253 desktopBootstrap.httpBaseUrl,
254 "desktop-managed",
255 "http-base-url",
256 ),
257 wsBaseUrl: normalizeBaseUrl(
258 desktopBootstrap.wsBaseUrl,
259 "desktop-managed",
260 "websocket-base-url",
261 ),
133262 },
134263 };
135264}
......@@ -139,11 +268,12 @@ export function resolvePrimaryEnvironmentHttpUrl(
139268 searchParams?: Record<string, string>,
140269): string {
141270 const primaryTarget = readPrimaryEnvironmentTarget();
142 if (!primaryTarget) {
143 throw new Error("Unable to resolve the primary environment HTTP base URL.");
144 }
145271
146 const url = new URL(resolveHttpRequestBaseUrl(primaryTarget.target.httpBaseUrl));
272 const url = parseTargetUrl({
273 rawValue: resolveHttpRequestBaseUrl(primaryTarget),
274 source: primaryTarget.source,
275 urlKind: "http-base-url",
276 });
147277 url.pathname = pathname;
148278 if (searchParams) {
149279 url.search = new URLSearchParams(searchParams).toString();
......@@ -151,7 +281,7 @@ export function resolvePrimaryEnvironmentHttpUrl(
151281 return url.toString();
152282}
153283
154export function readPrimaryEnvironmentTarget(): PrimaryEnvironmentTarget | null {
284export function readPrimaryEnvironmentTarget(): PrimaryEnvironmentTarget {
155285 return (
156286 resolveDesktopPrimaryTarget() ??
157287 resolveConfiguredPrimaryTarget() ??