Repository: remix-run/remix (ts)

Issue:
Scheme not set to HTTPS when TLS config options are set in remix.config.js
### What version of Remix are you using?

1.18

### Are all your remix dependencies & dev-dependencies using the same version?

- [X] Yes

### Steps to Reproduce

Set tlsKey and tlsCert in remix.config.js for v2_dev

### Expected Behavior

scheme gets set to HTTPS

### Actual Behavior

scheme gets set to HTTP

Pull request:
fix(dev): better networking options
Fixes #6718
Supercedes #5467

Tested my running creating TLS key/cert for `myhost` and running:

```sh
REMIX_DEV_ORIGIN=https://myhost remix dev --no-restart -c \"node server.js\" --tls-key=key.pem --tls-cert=cert.pem --port=443
```

👆 Note that origin can omit the port since the default HTTPS port of 443 is used. Also tested with `tlsKey` and `tlsCert` config options.

Also tried with the same command without `REMIX_DEV_ORIGIN` to ensure that TLS protocols (`https`/`wss`) are correctly inferred from TLS flags and config options.

Finally, also tested that `REMIX_DEV_SERVER_WS_PORT` works to set the ws port for the old dev server.

## TODO

- [x] docs
- [x] changeset

Patch:
diff --git a/.changeset/dry-items-deliver.md b/.changeset/dry-items-deliver.md
new file mode 100644
index 000000000..b4cc90a3d
--- /dev/null
+++ b/.changeset/dry-items-deliver.md
@@ -0,0 +1,9 @@
+---
+"@remix-run/dev": minor
+"@remix-run/react": minor
+"@remix-run/server-runtime": minor
+---
+
+improved networking options for v2_dev
+
+deprecate the `--scheme` and `--host` options and replace them with the `REMIX_DEV_ORIGIN` environment variable
diff --git a/docs/other-api/dev-v2.md b/docs/other-api/dev-v2.md
index 698020837..22dd4ecba 100644
--- a/docs/other-api/dev-v2.md
+++ b/docs/other-api/dev-v2.md
@@ -136,24 +136,23 @@ but CloudFlare does not support async I/O like `fetch` outside of request handli
 
 Options priority order is: 1. flags, 2. config, 3. defaults.
 
-| Option          | flag               | config           | default                                           |
-| --------------- | ------------------ | ---------------- | ------------------------------------------------- |
-| Command         | `-c` / `--command` | `command`        | `remix-serve <server build path>`                 |
-| No restart      | `--no-restart`     | `restart: false` | `restart: true`                                   |
-| Scheme          | `--scheme`         | `scheme`         | `https` if TLS key/cert are set, otherwise `http` |
-| Host            | `--host`           | `host`           | `localhost`                                       |
-| Port            | `--port`           | `port`           | Dynamically chosen open port                      |
-| TLS key         | `--tls-key`        | `tlsKey`         | N/A                                               |
-| TLS certificate | `--tls-cert`       | `tlsCert`        | N/A                                               |
+| Option          | flag               | config           | default                           |
+| --------------- | ------------------ | ---------------- | --------------------------------- |
+| Command         | `-c` / `--command` | `command`        | `remix-serve <server build path>` |
+| No restart      | `--no-restart`     | `restart: false` | `restart: true`                   |
+| Port            | `--port`           | `port`           | Dynamically chosen open port      |
+| TLS key         | `--tls-key`        | `tlsKey`         | N/A                               |
+| TLS certificate | `--tls-cert`       | `tlsCert`        | N/A                               |
 
 <docs-info>
 
-The scheme/host/port options only affect the Remix dev server, and **do not affect your app server**.
+The port option only affects the Remix dev server, and **does not affect your app server**.
 Your app will run on your app server's normal URL.
 
-You most likely won't want to configure the scheme/host/port for the dev server,
-as those are implementation details used internally for hot updates.
-They exist in case you need fine-grain control, for example Docker networking or using specific open ports.
+You probably don't want to configure the port for the dev server,
+as it is an implementation detail used internally for hot updates.
+The port option exists in case you need fine-grain networking control,
+for example to setup Docker networking or use a specific open port for security purposes.
 
 </docs-info>
 
@@ -244,9 +243,11 @@ To use [Mock Service Worker][msw] in development, you'll need to:
 1. Run MSW as part of your app server
 2. Configure MSW to not mock internal "dev ready" messages to the dev server
 
+`remix dev` will provide the `REMIX_DEV_ORIGIN` environment variable for use in your app server.
+
 For example, if you are using [binode][binode] to integrate with MSW,
 make sure that the call to `binode` is within the `remix dev -c` subcommand.
-That way, the MSW server will have access to the `REMIX_DEV_HTTP_ORIGIN` environment variable:
+That way, the MSW server will have access to the `REMIX_DEV_ORIGIN` environment variable:
 
 ```json filename=package.json
 {
@@ -257,16 +258,16 @@ That way, the MSW server will have access to the `REMIX_DEV_HTTP_ORIGIN` environ
 }
 ```
 
-Next, you can use `REMIX_DEV_HTTP_ORIGIN` to let MSW forward internal "dev ready" messages on `/ping`:
+Next, you can use `REMIX_DEV_ORIGIN` to let MSW forward internal "dev ready" messages on `/ping`:
 
 ```ts
 import { rest } from "msw";
 
+let REMIX_DEV_PING = new URL(process.env.REMIX_DEV_ORIGIN);
+REMIX_DEV_PING.pathname = "/ping";
+
 export const server = setupServer(
-  rest.post(
-    `${process.env.REMIX_DEV_HTTP_ORIGIN}/ping`,
-    (req) => req.passthrough()
-  )
+  rest.post(REMIX_DEV_PING.href, (req) => req.passthrough())
   // ... other request handlers go here ...
 );
 ```
@@ -338,6 +339,31 @@ remix dev --tls-key=key.pem --tls-cert=cert.pem -c 'node ./server.js'
 Alternatively, you can specify the TLS key and cert via the `v2_dev.tlsCert` and `v2_dev.tlsKey` config options.
 Now your app server and dev server are TLS ready!
 
+### How to integrate with a reverse proxy
+
+Let's say you have the app server and dev server both running on the same machine:
+
+- App server 👉 `http://localhost:1234`
+- Dev server 👉 `http://localhost:5678`
+
+Then, you setup a reverse proxy in front of the app server and dev server:
+
+- Reverse proxy 👉 `https://myhost`
+
+But the internal HTTP and WebSocket connections to support hot updates will still try to reach the dev server's unproxied origin:
+
+- Hot updates 👉 `http://localhost:5678` / `ws://localhost:5678` ❌
+
+To get the internal connections to point to the reverse proxy, you can use the `REMIX_DEV_ORIGIN` environment variable:
+
+```sh
+REMIX_DEV_ORIGIN=https://myhost remix dev
+```
+
+Now, hot updates will be sent correctly to the proxy:
+
+- Hot updates 👉 `https://myhost` / `wss://myhost` ✅
+
 ### Troubleshooting
 
 #### HMR: hot updates losing app state
@@ -346,8 +372,7 @@ Hot Module Replacement is supposed to keep your app's state around between hot u
 But in some cases React cannot distinguish between existing components being changed and new components being added.
 [React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.
 
-Additionally, when adding or removing hooks, React Refresh treats that as a brand new component.
-So if you add `useLoaderData` to your component, you may lose state local to that component.
+Additionally, when adding or removing hooks, React Refresh treats that as a brand new component. So if you add `useLoaderData` to your component, you may lose state local to that component.
 
 These are limitations of React and [React Refresh][react-refresh], not Remix.
 
diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts
index 54a274f2d..0280942ad 100644
--- a/packages/remix-dev/cli/commands.ts
+++ b/packages/remix-dev/cli/commands.ts
@@ -174,8 +174,8 @@ export async function build(
     sourcemap,
   };
   if (mode === "development" && config.future.v2_dev) {
-    let origin = await resolveDevOrigin(config);
-    options.devOrigin = origin;
+    let resolved = await resolveDev(config);
+    options.REMIX_DEV_ORIGIN = resolved.REMIX_DEV_ORIGIN;
   }
 
   let fileWatchCache = createFileWatchCache();
@@ -238,7 +238,8 @@ export async function dev(
     return await new Promise(() => {});
   }
 
-  await devServer_unstable.serve(config, await resolveDevServe(config, flags));
+  let resolved = await resolveDevServe(config, flags);
+  await devServer_unstable.serve(config, resolved);
 }
 
 export async function codemod(
@@ -467,58 +468,72 @@ let parseMode = (
 
 let findPort = async () => getPort({ port: makeRange(3001, 3100) });
 
-type DevOrigin = {
-  scheme: string;
-  host: string;
-  port: number;
-};
-let resolveDevOrigin = async (
+let resolveDev = async (
   config: RemixConfig,
-  flags: Partial<DevOrigin> & {
+  flags: {
+    port?: number;
     tlsKey?: string;
     tlsCert?: string;
+    /** @deprecated */
+    scheme?: string; // TODO: remove in v2
+    /** @deprecated */
+    host?: string; // TODO: remove in v2
   } = {}
-): Promise<DevOrigin> => {
+) => {
   let dev = config.future.v2_dev;
   if (dev === false) throw Error("This should never happen");
 
-  // prettier-ignore
-  let scheme =
-    flags.scheme ??
-    (dev === true ? undefined : dev.scheme) ??
-    (flags.tlsKey && flags.tlsCert) ? "https": "http";
-  // prettier-ignore
-  let host =
-    flags.host ??
-    (dev === true ? undefined : dev.host) ??
-    "localhost";
   // prettier-ignore
   let port =
     flags.port ??
     (dev === true ? undefined : dev.port) ??
     (await findPort());
 
+  let tlsKey = flags.tlsKey ?? (dev === true ? undefined : dev.tlsKey);
+  if (tlsKey) tlsKey = path.resolve(tlsKey);
+  let tlsCert = flags.tlsCert ?? (dev === true ? undefined : dev.tlsCert);
+  if (tlsCert) tlsCert = path.resolve(tlsCert);
+  let isTLS = tlsKey && tlsCert;
+
+  let REMIX_DEV_ORIGIN = process.env.REMIX_DEV_ORIGIN;
+  if (REMIX_DEV_ORIGIN === undefined) {
+    // prettier-ignore
+    let scheme =
+      flags.scheme ?? // TODO: remove in v2
+      (dev === true ? undefined : dev.scheme) ?? // TODO: remove in v2
+      isTLS ? "https" : "http";
+    // prettier-ignore
+    let hostname =
+      flags.host ?? // TODO: remove in v2
+      (dev === true ? undefined : dev.host) ?? // TODO: remove in v2
+      "localhost";
+    REMIX_DEV_ORIGIN = `${scheme}://${hostname}:${port}`;
+  }
+
   return {
-    scheme,
-    host,
     port,
+    tlsKey,
+    tlsCert,
+    REMIX_DEV_ORIGIN: new URL(REMIX_DEV_ORIGIN),
   };
 };
 
-type DevServeFlags = DevOrigin & {
-  command?: string;
-  restart: boolean;
-  tlsKey?: string;
-  tlsCert?: string;
-};
 let resolveDevServe = async (
   config: RemixConfig,
-  flags: Partial<DevServeFlags> = {}
-): Promise<DevServeFlags> => {
+  flags: {
+    port?: number;
+    tlsKey?: string;
+    tlsCert?: string;
+    scheme?: string; // TODO: remove in v2
+    host?: string; // TODO: remove in v2
+    command?: string;
+    restart?: boolean;
+  } = {}
+) => {
   let dev = config.future.v2_dev;
   if (dev === false) throw Error("Cannot resolve dev options");
 
-  let origin = await resolveDevOrigin(config, flags);
+  let resolved = await resolveDev(config, flags);
 
   // prettier-ignore
   let command =
@@ -528,16 +543,9 @@ let resolveDevServe = async (
   let restart =
     flags.restart ?? (dev === true ? undefined : dev.restart) ?? true;
 
-  let tlsKey = flags.tlsKey ?? (dev === true ? undefined : dev.tlsKey);
-  if (tlsKey) tlsKey = path.resolve(tlsKey);
-  let tlsCert = flags.tlsCert ?? (dev === true ? undefined : dev.tlsCert);
-  if (tlsCert) tlsCert = path.resolve(tlsCert);
-
   return {
+    ...resolved,
     command,
-    ...origin,
     restart,
-    tlsKey,
-    tlsCert,
   };
 };
diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts
index df1cdcaa4..7f14c1f5b 100644
--- a/packages/remix-dev/cli/run.ts
+++ b/packages/remix-dev/cli/run.ts
@@ -9,6 +9,7 @@ import * as colors from "../colors";
 import * as commands from "./commands";
 import { validateNewProjectPath, validateTemplate } from "./create";
 import { detectPackageManager } from "./detectPackageManager";
+import { logger } from "../tux";
 
 const helpText = `
 ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(
@@ -44,8 +45,6 @@ ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow(
 
     [v2_dev]
     --command, -c       Command used to run your app server
-    --scheme            Scheme for the dev server. Default: http
-    --host              Host for the dev server. Default: localhost
     --port              Port for the dev server. Default: any open port
     --no-restart        Do not restart the app server when rebuilds occur.
     --tls-key           Path to TLS key (key.pem)
@@ -183,13 +182,15 @@ export async function run(argv: string[] = process.argv.slice(2)) {
       // dev server
       "--command": String,
       "-c": "--command",
-      "--scheme": String,
-      "--host": String,
       "--port": Number,
       "-p": "--port",
       "--no-restart": Boolean,
       "--tls-key": String,
       "--tls-cert": String,
+
+      // deprecated, remove in v2
+      "--scheme": String,
+      "--host": String,
     },
     {
       argv,
@@ -214,6 +215,25 @@ export async function run(argv: string[] = process.argv.slice(2)) {
     return;
   }
 
+  // TODO: remove in v2
+  if (flags["scheme"]) {
+    logger.warn("`--scheme` flag is deprecated", {
+      details: [
+        "Use `REMIX_DEV_ORIGIN` instead",
+        "-> https://remix.run/docs/en/main/other-api/dev-v2#how-to-integrate-with-a-reverse-proxy",
+      ],
+    });
+  }
+  // TODO: remove in v2
+  if (flags["host"]) {
+    logger.warn("`--host` flag is deprecated", {
+      details: [
+        "Use `REMIX_DEV_ORIGIN` instead",
+        "-> https://remix.run/docs/en/main/other-api/dev-v2#how-to-integrate-with-a-reverse-proxy",
+      ],
+    });
+  }
+
   if (flags["tls-key"]) {
     flags.tlsKey = flags["tls-key"];
     delete flags["tls-key"];
diff --git a/packages/remix-dev/compiler/js/compiler.ts b/packages/remix-dev/compiler/js/compiler.ts
index b8c7b2657..407b5af45 100644
--- a/packages/remix-dev/compiler/js/compiler.ts
+++ b/packages/remix-dev/compiler/js/compiler.ts
@@ -136,6 +136,10 @@ const createEsbuildConfig = (
     publicPath: ctx.config.publicPath,
     define: {
       "process.env.NODE_ENV": JSON.stringify(ctx.options.mode),
+      "process.env.REMIX_DEV_ORIGIN": JSON.stringify(
+        ctx.options.REMIX_DEV_ORIGIN ?? ""
+      ),
+      // TODO: remove in v2
       "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify(
         ctx.config.devServerPort
       ),
diff --git a/packages/remix-dev/compiler/options.ts b/packages/remix-dev/compiler/options.ts
index 072f16ce1..2f85f6160 100644
--- a/packages/remix-dev/compiler/options.ts
+++ b/packages/remix-dev/compiler/options.ts
@@ -4,10 +4,5 @@ export type Options = {
   mode: Mode;
   sourcemap: boolean;
 
-  // TODO: required in v2
-  devOrigin?: {
-    scheme: string;
-    host: string;
-    port: number;
-  };
+  REMIX_DEV_ORIGIN?: URL; // TODO: required in v2
 };
diff --git a/packages/remix-dev/compiler/server/compiler.ts b/packages/remix-dev/compiler/server/compiler.ts
index 4fb76bdad..c1b995abe 100644
--- a/packages/remix-dev/compiler/server/compiler.ts
+++ b/packages/remix-dev/compiler/server/compiler.ts
@@ -104,12 +104,16 @@ const createEsbuildConfig = (
     publicPath: ctx.config.publicPath,
     define: {
       "process.env.NODE_ENV": JSON.stringify(ctx.options.mode),
-      // TODO: remove REMIX_DEV_SERVER_WS_PORT in v2
+      // TODO: remove in v2
       "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify(
         ctx.config.devServerPort
       ),
+      "process.env.REMIX_DEV_ORIGIN": JSON.stringify(
+        ctx.options.REMIX_DEV_ORIGIN ?? ""
+      ),
+      // TODO: remove in v2
       "process.env.REMIX_DEV_HTTP_ORIGIN": JSON.stringify(
-        ctx.options.devOrigin ?? "" // TODO: remove nullish check in v2
+        ctx.options.REMIX_DEV_ORIGIN ?? ""
       ),
     },
     jsx: "automatic",
diff --git a/packages/remix-dev/compiler/server/plugins/entry.ts b/packages/remix-dev/compiler/server/plugins/entry.ts
index 4966dd3ad..edb4fb60b 100644
--- a/packages/remix-dev/compiler/server/plugins/entry.ts
+++ b/packages/remix-dev/compiler/server/plugins/entry.ts
@@ -50,13 +50,6 @@ ${Object.keys(config.routes)
   export const future = ${JSON.stringify(config.future)};
   export const publicPath = ${JSON.stringify(config.publicPath)};
   export const entry = { module: entryServer };
-  ${
-    options.devOrigin
-      ? `export const dev = ${JSON.stringify({
-          port: options.devOrigin.port,
-        })}`
-      : ""
-  }
   export const routes = {
     ${Object.keys(config.routes)
       .map((key, index) => {
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 73abaaa16..f6084a075 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -38,12 +38,15 @@ export type ServerPlatform = "node" | "neutral";
 
 type Dev = {
   command?: string;
-  scheme?: string;
-  host?: string;
   port?: number;
   restart?: boolean;
   tlsKey?: string;
   tlsCert?: string;
+
+  /** @deprecated remove in v2 */
+  scheme?: string;
+  /** @deprecated remove in v2 */
+  host?: string;
 };
 
 interface FutureConfig {
diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts
index 25cebb061..1abeb3641 100644
--- a/packages/remix-dev/devServer_unstable/index.ts
+++ b/packages/remix-dev/devServer_unstable/index.ts
@@ -24,14 +24,6 @@ import invariant from "../invariant";
 import { logger } from "../tux";
 import { kill, killtree } from "./proc";
 
-type Origin = {
-  scheme: string;
-  host: string;
-  port: number;
-};
-
-let stringifyOrigin = (o: Origin) => `${o.scheme}://${o.host}:${o.port}`;
-
 let detectBin = async (): Promise<string> => {
   let pkgManager = detectPackageManager() ?? "npm";
   if (pkgManager === "npm") {
@@ -47,12 +39,11 @@ export let serve = async (
   initialConfig: RemixConfig,
   options: {
     command?: string;
-    scheme: string;
-    host: string;
     port: number;
-    restart: boolean;
     tlsKey?: string;
     tlsCert?: string;
+    REMIX_DEV_ORIGIN: URL;
+    restart: boolean;
   }
 ) => {
   await loadEnv(initialConfig.rootDirectory);
@@ -92,12 +83,6 @@ export let serve = async (
       : http.createServer(app);
   let websocket = Socket.serve(server);
 
-  let origin: Origin = {
-    scheme: options.scheme,
-    host: options.host,
-    port: options.port,
-  };
-
   let bin = await detectBin();
   let startAppServer = (command?: string) => {
     let cmd =
@@ -113,7 +98,8 @@ export let serve = async (
           NODE_ENV: "development",
           PATH:
             bin + (process.platform === "win32" ? ";" : ":") + process.env.PATH,
-          REMIX_DEV_HTTP_ORIGIN: stringifyOrigin(origin),
+          REMIX_DEV_ORIGIN: options.REMIX_DEV_ORIGIN.href,
+          REMIX_DEV_HTTP_ORIGIN: options.REMIX_DEV_ORIGIN.href, // TODO: remove in v2
           FORCE_COLOR: process.env.NO_COLOR === undefined ? "1" : "0",
         },
         // https://github.com/sindresorhus/execa/issues/433
@@ -178,7 +164,7 @@ export let serve = async (
       options: {
         mode: "development",
         sourcemap: true,
-        devOrigin: origin,
+        REMIX_DEV_ORIGIN: options.REMIX_DEV_ORIGIN,
       },
       fileWatchCache,
       logger,
@@ -276,7 +262,7 @@ export let serve = async (
     }
   );
 
-  server.listen(origin.port);
+  server.listen(options.port);
 
   return new Promise(() => {}).finally(async () => {
     state.appServer?.pid && (await kill(state.appServer.pid));
diff --git a/packages/remix-dev/server-build.ts b/packages/remix-dev/server-build.ts
index 2098896e6..028c75c80 100644
--- a/packages/remix-dev/server-build.ts
+++ b/packages/remix-dev/server-build.ts
@@ -10,7 +10,6 @@ export const assets: ServerBuild["assets"] = undefined!;
 export const entry: ServerBuild["entry"] = undefined!;
 export const routes: ServerBuild["routes"] = undefined!;
 export const future: ServerBuild["future"] = undefined!;
-export const dev: ServerBuild["dev"] = undefined!;
 export const publicPath: ServerBuild["publicPath"] = undefined!;
 // prettier-ignore
 export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"] = undefined!;
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index c77492a54..ba367468e 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -1782,7 +1782,6 @@ export const LiveReload =
   process.env.NODE_ENV !== "development"
     ? () => null
     : function LiveReload({
-        // TODO: remove REMIX_DEV_SERVER_WS_PORT in v2
         port,
         timeoutMs = 1000,
         nonce = undefined,
@@ -1799,13 +1798,25 @@ export const LiveReload =
             dangerouslySetInnerHTML={{
               __html: js`
                 function remixLiveReloadConnect(config) {
-                  let protocol = location.protocol === "https:" ? "wss:" : "ws:";
-                  let host = location.hostname;
-                  let port = ${port} || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || ${Number(
-                process.env.REMIX_DEV_SERVER_WS_PORT || 8002
-              )};
-                  let socketPath = protocol + "//" + host + ":" + port + "/socket";
-                  let ws = new WebSocket(socketPath);
+                  let REMIX_DEV_ORIGIN = ${JSON.stringify(
+                    process.env.REMIX_DEV_ORIGIN
+                  )};
+                  let protocol =
+                    REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).protocol.replace(/^http/, "ws") :
+                    location.protocol === "https:" ? "wss:" : "ws:"; // remove in v2?
+                  let hostname = location.hostname;
+                  let url = new URL(protocol + "//" + hostname + "/socket");
+
+                  url.port =
+                    ${port} ||
+                    REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port :
+                    Number(${
+                      // TODO: remove in v2
+                      process.env.REMIX_DEV_SERVER_WS_PORT
+                    }) ||
+                    8002;
+
+                  let ws = new WebSocket(url.href);
                   ws.onmessage = async (message) => {
                     let event = JSON.parse(message.data);
                     if (event.type === "LOG") {
diff --git a/packages/remix-server-runtime/build.ts b/packages/remix-server-runtime/build.ts
index 7a5cbc5c6..00c5e8298 100644
--- a/packages/remix-server-runtime/build.ts
+++ b/packages/remix-server-runtime/build.ts
@@ -15,7 +15,6 @@ export interface ServerBuild {
   publicPath: string;
   assetsBuildDirectory: string;
   future: FutureConfig;
-  dev?: { port: number };
 }
 
 export interface HandleDocumentRequestFunction {
diff --git a/packages/remix-server-runtime/dev.ts b/packages/remix-server-runtime/dev.ts
index 2163e36b4..f6da046a5 100644
--- a/packages/remix-server-runtime/dev.ts
+++ b/packages/remix-server-runtime/dev.ts
@@ -1,16 +1,25 @@
 import type { ServerBuild } from "./build";
 
-export function broadcastDevReady(build: ServerBuild, origin?: string) {
+export async function broadcastDevReady(build: ServerBuild, origin?: string) {
   origin ??= process.env.REMIX_DEV_HTTP_ORIGIN;
   if (!origin) throw Error("Dev server origin not set");
+  let url = new URL(origin);
+  url.pathname = "ping";
 
-  fetch(`${origin}/ping`, {
+  let response = await fetch(url.href, {
     method: "POST",
     headers: { "Content-Type": "application/json" },
     body: JSON.stringify({ buildHash: build.assets.version }),
   }).catch((error) => {
-    console.error(`Could not reach Remix dev server at ${origin}`);
+    console.error(`Could not reach Remix dev server at ${url}`);
+    throw error;
   });
+  if (!response.ok) {
+    console.error(
+      `Could not reach Remix dev server at ${url} (${response.status})`
+    );
+    throw Error(await response.text());
+  }
 }
 
 export function logDevReady(build: ServerBuild) {
diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts
index 66e0e7e25..eb30cb36f 100644
--- a/packages/remix-server-runtime/server.ts
+++ b/packages/remix-server-runtime/server.ts
@@ -296,7 +296,6 @@ async function handleDocumentRequestRR(
         errors: serializeErrors(context.errors, serverMode),
       },
       future: build.future,
-      dev: build.dev,
     }),
     future: build.future,
   };
diff --git a/packages/remix-server-runtime/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts
index 07bdee231..583f2a21d 100644
--- a/packages/remix-server-runtime/serverHandoff.ts
+++ b/packages/remix-server-runtime/serverHandoff.ts
@@ -21,7 +21,6 @@ export function createServerHandoffString<T>(serverHandoff: {
   state: ValidateShape<T, HydrationState>;
   url: string;
   future: FutureConfig;
-  dev?: { port: number };
 }): string {
   // Uses faster alternative of jsesc to escape data returned from the loaders.
   // This string is inserted directly into the HTML in the `<Scripts>` element.


Test patch:
diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts
index 20a0a4905..75160fd01 100644
--- a/packages/remix-dev/__tests__/cli-test.ts
+++ b/packages/remix-dev/__tests__/cli-test.ts
@@ -118,8 +118,6 @@ describe("remix CLI", () => {
 
             [v2_dev]
             --command, -c       Command used to run your app server
-            --scheme            Scheme for the dev server. Default: http
-            --host              Host for the dev server. Default: localhost
             --port              Port for the dev server. Default: any open port
             --no-restart        Do not restart the app server when rebuilds occur.
             --tls-key           Path to TLS key (key.pem)
diff --git a/packages/remix-react/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx
index 6cb621925..748f4b12c 100644
--- a/packages/remix-react/__tests__/components-test.tsx
+++ b/packages/remix-react/__tests__/components-test.tsx
@@ -47,14 +47,14 @@ describe("<LiveReload />", () => {
       LiveReload = require("../components").LiveReload;
       let { container } = render(<LiveReload />);
       expect(container.querySelector("script")).toHaveTextContent(
-        "let port = undefined || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 8002;"
+        "url.port = undefined || REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : Number(undefined) || 8002;"
       );
     });
 
     it("can set the port explicitly", () => {
       let { container } = render(<LiveReload port={4321} />);
       expect(container.querySelector("script")).toHaveTextContent(
-        "let port = 4321 || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 8002;"
+        "url.port = 4321 || REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : Number(undefined) || 8002;"
       );
     });
 
@@ -62,7 +62,7 @@ describe("<LiveReload />", () => {
       process.env.REMIX_DEV_SERVER_WS_PORT = "1234";
       let { container } = render(<LiveReload />);
       expect(container.querySelector("script")).toHaveTextContent(
-        "let port = undefined || (window.__remixContext && window.__remixContext.dev && window.__remixContext.dev.port) || 1234;"
+        "url.port = undefined || REMIX_DEV_ORIGIN ? new URL(REMIX_DEV_ORIGIN).port : Number(1234) || 8002;"
       );
     });