Repository: remix-run/remix
Pull request #3064 (2022-05-02 21:51:29)

fix: provide clear error messages for create-remix errors
This PR clears up some error messaging when we cannot validate `--template` options.  We had some nested `try`/`catch`'s where specific errors were getting swallowed by an outer `catch` with a more generic error.

Before - with a non-existent github template that would return a 404:
```sh
~> npx create-remix@latest --template brophdawg11/junk
🚨 There was a problem verifying the template. Please ensure you are connected to the internet and try again later.
```

After (stack trace is only because of the direct `node` execution):
```sh
~> node ../remix/build/node_modules/@remix-run/dev/cli create --template brophdawg11/junk
Error: 🚨 The template could not be verified. Please double check that the template is a valid GitHub repository and try again.
    at Object.validateTemplate (/Users/matt/me/remix/repos/remix/build/node_modules/@remix-run/dev/cli/create.js:492:19)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.run (/Users/matt/me/remix/repos/remix/build/node_modules/@remix-run/dev/cli/run.js:274:11)
```

- [x] Tests

Testing Strategy:
Added unit tests against specific `msw` mocks to force various HTTP errors (400, 403, 404)


Patch:
diff --git a/packages/remix-dev/cli/create.ts b/packages/remix-dev/cli/create.ts
index 83a3e8750..0264e41e3 100644
--- a/packages/remix-dev/cli/create.ts
+++ b/packages/remix-dev/cli/create.ts
@@ -502,69 +502,73 @@ export async function validateTemplate(input: string) {
     }
     case "remoteTarball": {
       let spinner = ora("Validating the template file…").start();
+      let response;
       try {
-        let response = await fetch(input, { method: "HEAD" });
-        spinner.stop();
-        switch (response.status) {
-          case 200:
-            return;
-          case 404:
-            throw Error(
-              "🚨 The template file could not be verified. Please double check " +
-                "the URL and try again."
-            );
-          default:
-            throw Error(
-              "🚨 The template file could not be verified. The server returned " +
-                `a response with a ${response.status} status. Please double ` +
-                "check the URL and try again."
-            );
-        }
-      } catch (err) {
-        spinner.stop();
+        response = await fetch(input, { method: "HEAD" });
+      } catch (_) {
         throw Error(
           "🚨 There was a problem verifying the template file. Please ensure " +
             "you are connected to the internet and try again later."
         );
+      } finally {
+        spinner.stop();
+      }
+
+      switch (response.status) {
+        case 200:
+          return;
+        case 404:
+          throw Error(
+            "🚨 The template file could not be verified. Please double check " +
+              "the URL and try again."
+          );
+        default:
+          throw Error(
+            "🚨 The template file could not be verified. The server returned " +
+              `a response with a ${response.status} status. Please double ` +
+              "check the URL and try again."
+          );
       }
     }
     case "repo": {
       let spinner = ora("Validating the template repo…").start();
       let { url, filePath } = getRepoInfo(input);
+      let response;
       try {
-        let response = await fetch(url, { method: "HEAD" });
-        spinner.stop();
-        switch (response.status) {
-          case 200:
-            return;
-          case 403:
-            throw Error(
-              "🚨 The template could not be verified because you do not have " +
-                "access to the repository. Please double check the access " +
-                "rights of this repo and try again."
-            );
-          case 404:
-            throw Error(
-              "🚨 The template could not be verified. Please double check that " +
-                "the template is a valid GitHub repository" +
-                (filePath && filePath !== "/"
-                  ? " and that the filepath points to a directory in the repo"
-                  : "") +
-                " and try again."
-            );
-          default:
-            throw Error(
-              "🚨 The template could not be verified. The server returned a " +
-                `response with a ${response.status} status. Please double check ` +
-                "that the template is a valid GitHub repository  and try again."
-            );
-        }
+        response = await fetch(url, { method: "HEAD" });
       } catch (_) {
-        spinner.stop();
         throw Error(
-          "🚨 There was a problem verifying the template. Please ensure you " +
+          "🚨 There was a problem fetching the template. Please ensure you " +
             "are connected to the internet and try again later."
         );
+      } finally {
+        spinner.stop();
+      }
+
+      switch (response.status) {
+        case 200:
+          return;
+        case 403:
+          throw Error(
+            "🚨 The template could not be verified because you do not have " +
+              "access to the repository. Please double check the access " +
+              "rights of this repo and try again."
+          );
+        case 404:
+          throw Error(
+            "🚨 The template could not be verified. Please double check that " +
+              "the template is a valid GitHub repository" +
+              (filePath && filePath !== "/"
+                ? " and that the filepath points to a directory in the repo"
+                : "") +
+              " and try again."
+          );
+        default:
+          throw Error(
+            "🚨 The template could not be verified. The server returned a " +
+              `response with a ${response.status} status. Please double check ` +
+              "that the template is a valid GitHub repository and try again."
+          );
       }
     }
     case "example":
@@ -576,34 +580,36 @@ export async function validateTemplate(input: string) {
       }
       let typeDir = templateType + "s";
       let templateUrl = `https://github.com/remix-run/remix/tree/main/${typeDir}/${name}`;
+      let response;
       try {
-        let response = await fetch(templateUrl, { method: "HEAD" });
-        spinner.stop();
-        switch (response.status) {
-          case 200:
-            return;
-          case 404:
-            throw Error(
-              "🚨 The template could not be verified. Please double check that " +
-                "the template is a valid project directory in " +
-                `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
-                "try again."
-            );
-          default:
-            throw Error(
-              "🚨 The template could not be verified. The server returned a " +
-                `response with a ${response.status} status. Please double ` +
-                "check that the template is a valid project directory in " +
-                `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
-                "try again."
-            );
-        }
+        response = await fetch(templateUrl, { method: "HEAD" });
       } catch (_) {
-        spinner.stop();
         throw Error(
           "🚨 There was a problem verifying the template. Please ensure you are " +
             "connected to the internet and try again later."
         );
+      } finally {
+        spinner.stop();
+      }
+
+      switch (response.status) {
+        case 200:
+          return;
+        case 404:
+          throw Error(
+            "🚨 The template could not be verified. Please double check that " +
+              "the template is a valid project directory in " +
+              `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
+              "try again."
+          );
+        default:
+          throw Error(
+            "🚨 The template could not be verified. The server returned a " +
+              `response with a ${response.status} status. Please double ` +
+              "check that the template is a valid project directory in " +
+              `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
+              "try again."
+          );
       }
     }
   }


Test patch:
diff --git a/packages/remix-dev/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts
index 74cd1fa67..9e2dda96b 100644
--- a/packages/remix-dev/__tests__/create-test.ts
+++ b/packages/remix-dev/__tests__/create-test.ts
@@ -542,6 +542,88 @@ describe("the create command", () => {
     expect(output).toContain(getOptOutOfInstallMessage("pnpm exec remix init"));
     process.env.npm_user_agent = originalUserAgent;
   });
+
+  describe("errors", () => {
+    it("identifies when a github repo is not accessible (403)", async () => {
+      let projectDir = await getProjectDir("repo");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "error-username/403",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template could not be verified because you do not have access to the repository. Please double check the access rights of this repo and try again.]`
+      );
+    });
+
+    it("identifies when a github repo does not exist (404)", async () => {
+      let projectDir = await getProjectDir("repo");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "error-username/404",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template could not be verified. Please double check that the template is a valid GitHub repository and try again.]`
+      );
+    });
+
+    it("identifies when something unknown goes wrong with the repo request (4xx)", async () => {
+      let projectDir = await getProjectDir("repo");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "error-username/400",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template could not be verified. The server returned a response with a 400 status. Please double check that the template is a valid GitHub repository and try again.]`
+      );
+    });
+
+    it("identifies when a remote tarball does not exist (404)", async () => {
+      let projectDir = await getProjectDir("remote-tarball");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "https://example.com/error/404/remix-stack.tar.gz",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template file could not be verified. Please double check the URL and try again.]`
+      );
+    });
+
+    it("identifies when a remote tarball does not exist (4xx)", async () => {
+      let projectDir = await getProjectDir("remote-tarball");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "https://example.com/error/400/remix-stack.tar.gz",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template file could not be verified. The server returned a response with a 400 status. Please double check the URL and try again.]`
+      );
+    });
+  });
 });
 
 function getSuccessMessage(projectDirectory: string) {
diff --git a/packages/remix-dev/__tests__/github-mocks.ts b/packages/remix-dev/__tests__/github-mocks.ts
index 56718f8c0..8e486be7b 100644
--- a/packages/remix-dev/__tests__/github-mocks.ts
+++ b/packages/remix-dev/__tests__/github-mocks.ts
@@ -76,6 +76,12 @@ let githubHandlers: Array<RequestHandler> = [
       return res(ctx.status(200));
     }
   ),
+  rest.head(
+    `https://github.com/error-username/:status`,
+    async (req, res, ctx) => {
+      return res(ctx.status(Number(req.params.status)));
+    }
+  ),
   rest.head(`https://github.com/:owner/:repo`, async (req, res, ctx) => {
     return res(ctx.status(200));
   }),
diff --git a/packages/remix-dev/__tests__/msw.ts b/packages/remix-dev/__tests__/msw.ts
index 409603c3d..b73408dd1 100644
--- a/packages/remix-dev/__tests__/msw.ts
+++ b/packages/remix-dev/__tests__/msw.ts
@@ -8,6 +8,12 @@ import { githubHandlers } from "./github-mocks";
 type RequestHandler = Parameters<typeof setupServer>[0];
 
 let miscHandlers: Array<RequestHandler> = [
+  rest.head(
+    "https://example.com/error/:status/remix-stack.tar.gz",
+    async (req, res, ctx) => {
+      return res(ctx.status(Number(req.params.status)));
+    }
+  ),
   rest.head("https://example.com/remix-stack.tar.gz", async (req, res, ctx) => {
     return res(ctx.status(200));
   }),