Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Improve catching of errors when loading routes
Sebastian Martinez committed 2 years ago
commit d216caec7d49c51222d8d40963b9fd91e4b671fc
parent 8450f0cd9cdba4607ea7e8f1c3bc6f1279ea9718
24 files changed +388 -184
modified src/App.svelte
@@ -24,7 +24,7 @@
  import Session from "@app/views/session/Index.svelte";
  import Source from "@app/views/projects/Source.svelte";

-
  import LoadError from "@app/components/LoadError.svelte";
+
  import Error from "@app/views/error/View.svelte";
  import Loading from "@app/components/Loading.svelte";

  const activeRouteStore = router.activeRouteStore;
@@ -87,8 +87,8 @@
  <Patches {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "project.patch"}
  <Patch {...$activeRouteStore.params} />
-
{:else if $activeRouteStore.resource === "loadError"}
-
  <LoadError {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "error"}
+
  <Error {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "notFound"}
  <NotFound {...$activeRouteStore.params} />
{:else}
modified src/App/Header/Breadcrumbs.svelte
@@ -20,7 +20,7 @@
  }
</style>

-
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "session" || $activeRouteStore.resource === "loadError" || $activeRouteStore.resource === "notFound"}
+
{#if $activeRouteStore.resource === "booting" || $activeRouteStore.resource === "home" || $activeRouteStore.resource === "session" || $activeRouteStore.resource === "error" || $activeRouteStore.resource === "notFound"}
  <!-- Don't render breadcrumbs for these routes. -->
{:else if $activeRouteStore.resource === "nodes"}
  <div class="breadcrumbs">
modified src/components/ErrorMessage.svelte
@@ -1,10 +1,20 @@
<script lang="ts">
+
  import type {
+
    ResponseError,
+
    ResponseParseError,
+
  } from "@httpd-client/lib/fetcher";
+
  import type { ComponentProps } from "svelte";
+

+
  import { config } from "@app/lib/config";
  import Command from "./Command.svelte";
  import ExternalLink from "./ExternalLink.svelte";
  import Icon from "./Icon.svelte";

-
  export let message: string;
-
  export let error: any | undefined = undefined;
+
  export let title: string;
+
  export let description: string;
+
  export let error: Error | ResponseParseError | ResponseError | undefined =
+
    undefined;
+
  export let icon: ComponentProps<Icon>["name"] = "alert";
</script>

<style>
@@ -19,6 +29,18 @@
    border-radius: var(--border-radius-small);
    gap: 1rem;
  }
+
  .label {
+
    font-size: var(--font-size-small);
+
    font-weight: var(--font-weight-regular);
+
    max-width: 36rem;
+
  }
+
  .error :global(code) {
+
    font-family: var(--font-family-monospace);
+
    font-size: var(--font-size-small);
+
    background-color: var(--color-fill-ghost);
+
    border-radius: var(--border-radius-tiny);
+
    padding: 0.125rem 0.25rem;
+
  }

  .help {
    font-size: var(--font-size-small);
@@ -27,8 +49,12 @@
</style>

<div class="error">
-
  <Icon name="alert" size="48" />
-
  {message}
+
  <Icon name={icon} size="48" />
+
  <div class="txt-medium txt-bold">
+
    {title}
+
  </div>
+
  <!-- This @html is secure since we don't allow user input -->
+
  <div class="label">{@html description}</div>
  {#if error}
    <div class="help">
      If you need help resolving this issue, copy the error message
@@ -40,11 +66,7 @@
    </div>
    <div style:max-width="25rem">
      <Command
-
        command={JSON.stringify({
-
          message: error.message,
-
          stack: error.stack,
-
          ...error,
-
        })}
+
        command={JSON.stringify(error, Object.getOwnPropertyNames(error))}
        fullWidth
        showPrompt={false} />
    </div>
deleted src/components/LoadError.svelte
@@ -1,58 +0,0 @@
-
<script lang="ts">
-
  import AppLayout from "@app/App/AppLayout.svelte";
-
  import Command from "./Command.svelte";
-
  import ExternalLink from "./ExternalLink.svelte";
-
  import Icon from "./Icon.svelte";
-

-
  export let title: string;
-
  export let errorMessage: string;
-
  export let stackTrace: string;
-
</script>
-

-
<style>
-
  .wrapper {
-
    gap: 1.5rem;
-
    height: 100%;
-
    display: flex;
-
    flex-direction: column;
-
    justify-content: center;
-
    align-items: center;
-
  }
-

-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    text-align: center;
-
    gap: 0.5rem;
-
  }
-

-
  .help {
-
    font-size: var(--font-size-small);
-
  }
-
</style>
-

-
<AppLayout>
-
  <div class="wrapper">
-
    <Icon name="desert" size="48" />
-
    <div class="container">
-
      <div class="txt-medium txt-bold">
-
        {title}
-
      </div>
-
      <div class="help">
-
        If you need help resolving this issue, copy the error message
-
        <br class="global-hide-on-mobile" />
-
        below and send it to us on
-
        <ExternalLink href="https://radicle.zulipchat.com">
-
          radicle.zulipchat.com
-
        </ExternalLink>
-
      </div>
-
    </div>
-

-
    <div style:max-width="25rem">
-
      <Command
-
        command={JSON.stringify({ errorMessage, stackTrace })}
-
        fullWidth
-
        showPrompt={false} />
-
    </div>
-
  </div>
-
</AppLayout>
modified src/lib/router.ts
@@ -122,8 +122,8 @@ function setTitle(loadedRoute: LoadedRoute) {

  if (loadedRoute.resource === "booting" || loadedRoute.resource === "home") {
    title.push("Radicle");
-
  } else if (loadedRoute.resource === "loadError") {
-
    title.push("Load error");
+
  } else if (loadedRoute.resource === "error") {
+
    title.push("Error");
    title.push("Radicle");
  } else if (loadedRoute.resource === "notFound") {
    title.push("Page not found");
@@ -259,7 +259,7 @@ export function routeToPath(route: Route): string {
  } else if (
    route.resource === "booting" ||
    route.resource === "notFound" ||
-
    route.resource === "loadError"
+
    route.resource === "error"
  ) {
    return "";
  } else {
modified src/lib/router/definitions.ts
@@ -1,5 +1,9 @@
import type { HomeRoute, HomeLoadedRoute } from "@app/views/home/router";
import type {
+
  ResponseParseError,
+
  ResponseError,
+
} from "@httpd-client/lib/fetcher";
+
import type {
  ProjectLoadedRoute,
  ProjectRoute,
} from "@app/views/projects/router";
@@ -29,19 +33,19 @@ interface SessionRoute {
  };
}

-
export interface LoadErrorRoute {
-
  resource: "loadError";
+
export interface ErrorRoute {
+
  resource: "error";
  params: {
    title: string;
-
    errorMessage: string;
-
    stackTrace: string;
+
    description: string;
+
    error: Error | ResponseError | ResponseParseError;
  };
}

export type Route =
  | BootingRoute
  | HomeRoute
-
  | LoadErrorRoute
+
  | ErrorRoute
  | NotFoundRoute
  | ProjectRoute
  | NodesRoute
@@ -50,7 +54,7 @@ export type Route =
export type LoadedRoute =
  | BootingRoute
  | HomeLoadedRoute
-
  | LoadErrorRoute
+
  | ErrorRoute
  | NotFoundRoute
  | ProjectLoadedRoute
  | NodesLoadedRoute
modified src/lib/utils.ts
@@ -1,4 +1,4 @@
-
import type { Comment } from "@httpd-client";
+
import type { BaseUrl, Comment } from "@httpd-client";

import md5 from "md5";
import bs58 from "bs58";
@@ -93,6 +93,10 @@ export function formatEditedCaption(lastEdit: Comment["edits"][0]) {
  } edited ${formatTimestamp(lastEdit.timestamp / 1000)}`;
}

+
export function baseUrlToUrl(baseUrl: BaseUrl): URL {
+
  return new URL(`${baseUrl.scheme}://${baseUrl.hostname}:${baseUrl.port}`);
+
}
+

// Generates a publicly shareable link.
export function formatPublicExplorer(
  publicExplorer: string,
added src/views/error/View.svelte
@@ -0,0 +1,38 @@
+
<script lang="ts">
+
  import type { ComponentProps } from "svelte";
+

+
  import AppLayout from "@app/App/AppLayout.svelte";
+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
+

+
  export let title: string;
+
  export let description: string;
+
  export let error: ComponentProps<ErrorMessage>["error"];
+
</script>
+

+
<style>
+
  .wrapper {
+
    padding: 4rem 0 2rem 0;
+
    gap: 1.5rem;
+
    height: 100%;
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: center;
+
    align-items: center;
+
  }
+

+
  .container {
+
    display: flex;
+
    flex-direction: column;
+
    align-items: center;
+
    text-align: center;
+
    gap: 0.5rem;
+
  }
+
</style>
+

+
<AppLayout>
+
  <div class="wrapper">
+
    <div class="container">
+
      <ErrorMessage icon="desert" {title} {description} {error} />
+
    </div>
+
  </div>
+
</AppLayout>
modified src/views/home/Index.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
-
  import type { ProjectWithListingData } from "@app/lib/projects";
  import type { BaseUrl } from "@httpd-client";
+
  import type { ProjectWithListingData } from "@app/lib/projects";
+
  import type { ComponentProps } from "svelte";

  import storedWritable from "@efstajas/svelte-stored-writable";
  import { HttpdClient } from "@httpd-client";
@@ -9,15 +10,17 @@

  import { api, httpdStore } from "@app/lib/httpd";
  import { deduplicateStore } from "@app/lib/deduplicateStore";
+
  import { baseUrlToUrl } from "@app/lib/utils";
  import { getProjectsListingData } from "@app/lib/projects";
+
  import { handleError } from "@app/views/home/error";
  import { isDelegate } from "@app/lib/roles";
  import { preferredSeeds } from "@app/lib/seeds";

  import AppLayout from "@app/App/AppLayout.svelte";
-
  import Button from "@app/components/Button.svelte";
  import ConnectInstructions from "@app/components/ConnectInstructions.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";

+
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import FilterButton from "./components/FilterButton.svelte";
  import HomepageSection from "./components/HomepageSection.svelte";
  import NewProjectButton from "./components/NewProjectButton.svelte";
@@ -36,8 +39,14 @@
    "all",
  );

-
  let localProjects: ProjectWithListingData[] | "error" | undefined;
-
  let preferredSeedProjects: ProjectWithListingData[] | "error" | undefined;
+
  let localProjects:
+
    | ProjectWithListingData[]
+
    | ComponentProps<ErrorMessage>["error"]
+
    | undefined;
+
  let preferredSeedProjects:
+
    | ProjectWithListingData[]
+
    | ComponentProps<ErrorMessage>["error"]
+
    | undefined;

  async function fetchProjects(baseUrl: BaseUrl, show: "all" | "pinned") {
    const api = new HttpdClient(baseUrl);
@@ -52,14 +61,10 @@
    return await getProjectsListingData(projects);
  }

-
  function handleProjectLoadError(): "error" {
-
    return "error";
-
  }
-

  async function loadLocalProjects() {
    localProjects = undefined;
    localProjects = await fetchProjects(api.baseUrl, "all").catch(
-
      handleProjectLoadError,
+
      error => error,
    );
  }

@@ -68,12 +73,12 @@

    if (!$selectedSeed) return;
    preferredSeedProjects = await fetchProjects($selectedSeed, "pinned").catch(
-
      handleProjectLoadError,
+
      error => error,
    );
  }

  function isSeeding(projectId: string) {
-
    if (localProjects === "error") return false;
+
    if (localProjects instanceof Error) return false;
    return localProjects?.some(p => p.project.id === projectId) ?? false;
  }

@@ -82,7 +87,7 @@
  $: $selectedSeed && void loadPreferredSeedProjects();
  $: filteredLocalProjects =
    $localProjectsFilter === "all" ||
-
    localProjects === "error" ||
+
    localProjects instanceof Error ||
    localProjects === undefined
      ? localProjects
      : localProjects.filter(p => isDelegate(nodeId, p.project.delegates));
@@ -114,9 +119,6 @@
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-regular);
  }
-
  .empty-state .action {
-
    margin-top: 0.5rem;
-
  }
  .project-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
@@ -140,11 +142,12 @@
    <div class="global-hide-on-mobile">
      <HomepageSection
        loading={$httpdStore.state !== "stopped" && localProjects === undefined}
-
        empty={localProjects === "error" ||
-
          $httpdStore.state === "stopped" ||
-
          !filteredLocalProjects?.length}
+
        empty={$httpdStore.state === "stopped" ||
+
          (filteredLocalProjects instanceof Array &&
+
            !filteredLocalProjects.length) ||
+
          localProjects instanceof Error}
        title="Local projects"
-
        subtitle="Projects you’re seeding with your local node">
+
        subtitle="Projects you're seeding with your local node">
        <svelte:fragment slot="actions">
          <FilterButton disabled={!nodeId} bind:value={$localProjectsFilter} />
          <NewProjectButton disabled={!nodeId} />
@@ -155,12 +158,12 @@
              <div style="text-align: left; width: 100%;">
                <ConnectInstructions />
              </div>
-
            {:else if localProjects === "error"}
-
              <div class="heading">Error loading projects</div>
-
              <div class="label">
-
                There was an error loading projects from your local node.
-
              </div>
-
              <div class="action"><Button>Learn more</Button></div>
+
            {:else if localProjects instanceof Error}
+
              <ErrorMessage
+
                {...handleError(
+
                  localProjects,
+
                  baseUrlToUrl(api.baseUrl).toString(),
+
                )} />
            {:else if !localProjects?.length}
              <div class="heading">No local projects</div>
              <div class="label">
@@ -175,7 +178,7 @@
          </div>
        </svelte:fragment>
        <div class="project-grid">
-
          {#if filteredLocalProjects && filteredLocalProjects !== "error"}
+
          {#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
            {#each filteredLocalProjects as { project, baseUrl, activity, lastCommit }}
              <ProjectCard
                id={project.id}
@@ -197,7 +200,7 @@

    <HomepageSection
      loading={preferredSeedProjects === undefined}
-
      empty={preferredSeedProjects === "error" ||
+
      empty={preferredSeedProjects instanceof Error ||
        preferredSeedProjects?.length === 0}
      title="Explore"
      subtitle="Pinned projects on your selected seed node">
@@ -212,11 +215,12 @@
      </svelte:fragment>
      <svelte:fragment slot="empty">
        <div class="empty-state">
-
          {#if preferredSeedProjects === "error"}
-
            <div class="heading">Something went wrong</div>
-
            <div class="label">
-
              There was an error loading projects from your preferred seed node.
-
            </div>
+
          {#if preferredSeedProjects instanceof Error}
+
            <ErrorMessage
+
              {...handleError(
+
                preferredSeedProjects,
+
                baseUrlToUrl(api.baseUrl).toString(),
+
              )} />
          {:else}
            <div class="heading">Nothing to see here</div>
            <div class="label">
@@ -226,7 +230,7 @@
        </div>
      </svelte:fragment>
      <div class="project-grid">
-
        {#if preferredSeedProjects && preferredSeedProjects !== "error"}
+
        {#if preferredSeedProjects && !(preferredSeedProjects instanceof Error)}
          {#each preferredSeedProjects as { project, baseUrl, activity, lastCommit }}
            <ProjectCard
              id={project.id}
added src/views/home/error.ts
@@ -0,0 +1,41 @@
+
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
+

+
export function handleError(
+
  error: Error | ResponseParseError | ResponseError,
+
  url: string,
+
): {
+
  error: Error | ResponseParseError | ResponseError;
+
  title: string;
+
  description: string;
+
} {
+
  if (error instanceof ResponseParseError) {
+
    return {
+
      error,
+
      title: "Could not parse the request",
+
      description:
+
        "The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
+
    };
+
  } else if (error instanceof ResponseError) {
+
    return {
+
      error,
+
      title: "Could not load the projects",
+
      description: `You're trying to fetch projects from a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
+
    };
+
  } else if (
+
    error instanceof TypeError &&
+
    error.message === "Failed to fetch"
+
  ) {
+
    return {
+
      error,
+
      title: "Resource not found",
+
      description: `You're trying to fetch a resource that is not available. Check that the ids is correct, and try to run <code>$ rad sync</code>.`,
+
    };
+
  } else {
+
    return {
+
      error,
+
      title: "Could not load this view",
+
      description:
+
        "You stumbled on an unknown error, we aren't exactly sure what happened.",
+
    };
+
  }
+
}
modified src/views/home/router.ts
@@ -1,5 +1,7 @@
-
import type { LoadErrorRoute } from "@app/lib/router/definitions";
+
import type { ErrorRoute } from "@app/lib/router/definitions";
+

import * as seeds from "@app/lib/seeds";
+

export interface HomeRoute {
  resource: "home";
}
@@ -9,9 +11,7 @@ export interface HomeLoadedRoute {
  params: Record<string, never>;
}

-
export async function loadHomeRoute(): Promise<
-
  HomeLoadedRoute | LoadErrorRoute
-
> {
+
export async function loadHomeRoute(): Promise<HomeLoadedRoute | ErrorRoute> {
  seeds.initialize();
  await seeds.waitForLoad();

modified src/views/nodes/View.svelte
@@ -178,7 +178,8 @@

      {#if error}
        <ErrorMessage
-
          message="Not able to load more projects from this node"
+
          title="Not able to load more projects from this node"
+
          description="You either loaded all remaining projects, or there was a network issue with this seed"
          {error} />
      {/if}
    </div>
added src/views/nodes/error.ts
@@ -0,0 +1,46 @@
+
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
+
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
+

+
export function handleError(
+
  error: Error | ResponseParseError | ResponseError,
+
  url: string,
+
): NotFoundRoute | ErrorRoute {
+
  if (error instanceof ResponseParseError) {
+
    return {
+
      resource: "error",
+
      params: {
+
        error,
+
        title: "Could not parse the request",
+
        description:
+
          "The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
+
      },
+
    };
+
  } else if (error instanceof ResponseError) {
+
    return {
+
      resource: "error",
+
      params: {
+
        error,
+
        title: "Could not load this node",
+
        description: `You're trying to access a node that is not reachable, make sure the address <a href="${url}">${url}</a> is correct and the right ports are exposed if its your node.`,
+
      },
+
    };
+
  } else if (
+
    error instanceof TypeError &&
+
    error.message === "Failed to fetch"
+
  ) {
+
    return {
+
      resource: "notFound",
+
      params: { title: "Node not found" },
+
    };
+
  } else {
+
    return {
+
      resource: "error",
+
      params: {
+
        error,
+
        title: "Could not load this node",
+
        description:
+
          "You stumbled on an unknown error, we aren't exactly sure what happened.",
+
      },
+
    };
+
  }
+
}
modified src/views/nodes/router.ts
@@ -1,13 +1,12 @@
import type { BaseUrl } from "@httpd-client";
-
import type {
-
  LoadErrorRoute,
-
  NotFoundRoute,
-
} from "@app/lib/router/definitions";
+
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type { ProjectWithListingData } from "@app/lib/projects";

import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
+
import { baseUrlToUrl } from "@app/lib/utils";
import { getProjectsListingData } from "@app/lib/projects";
+
import { handleError } from "@app/views/nodes/error";

export interface NodesRouteParams {
  baseUrl: BaseUrl;
@@ -70,7 +69,7 @@ export function nodePath(baseUrl: BaseUrl) {

export async function loadNodeRoute(
  params: NodesRouteParams,
-
): Promise<NodesLoadedRoute | NotFoundRoute | LoadErrorRoute> {
+
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
  const api = new HttpdClient(params.baseUrl);
  try {
    const projectPageIndex = 0;
@@ -91,22 +90,6 @@ export async function loadNodeRoute(
      },
    };
  } catch (error: any) {
-
    if (error.message === "Failed to fetch") {
-
      return {
-
        resource: "notFound",
-
        params: {
-
          title: "Node not found",
-
        },
-
      };
-
    } else {
-
      return {
-
        resource: "loadError",
-
        params: {
-
          title: "Not able to load this node",
-
          errorMessage: error.message,
-
          stackTrace: error.stackTrace,
-
        },
-
      };
-
    }
+
    return handleError(error, baseUrlToUrl(api.baseUrl).toString());
  }
}
modified src/views/projects/Cob/Revision.svelte
@@ -493,7 +493,10 @@
          class="diff-error txt-monospace txt-small"
          style:border-radius="var(--border-radius-small">
          <ErrorMessage
-
            message="Failed to load diff for this revision"
+
            title="Failed to load diff for this revision"
+
            description="Make sure you are able to connect to the seed <code>${utils
+
              .baseUrlToUrl(api.baseUrl)
+
              .toString()}</code>"
            {error} />
        </div>
      {/if}
modified src/views/projects/History.svelte
@@ -8,9 +8,10 @@
  } from "@httpd-client";
  import type { Route } from "@app/lib/router";

+
  import { COMMITS_PER_PAGE } from "./router";
  import { HttpdClient } from "@httpd-client";
+
  import { baseUrlToUrl } from "@app/lib/utils";
  import { groupCommits } from "@app/lib/commit";
-
  import { COMMITS_PER_PAGE } from "./router";

  import Button from "@app/components/Button.svelte";
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
@@ -157,7 +158,12 @@

  {#if error}
    <div class="message">
-
      <ErrorMessage message="Couldn't load commits" {error} />
+
      <ErrorMessage
+
        title="Couldn't load commits"
+
        description="Make sure you are able to connect to the seed <code>${baseUrlToUrl(
+
          api.baseUrl,
+
        ).toString()}</code>"
+
        {error} />
    </div>
  {/if}
</Layout>
modified src/views/projects/Issue/New.svelte
@@ -127,6 +127,7 @@
    </div>
  {:else}
    <ErrorMessage
-
      message="Couldn't access issue creation. Make sure you're authenticated." />
+
      title="Not able to create a new issue"
+
      description="Couldn't access issue creation. Make sure you're authenticated." />
  {/if}
</Layout>
modified src/views/projects/Issues.svelte
@@ -5,7 +5,7 @@
  import { ISSUES_PER_PAGE } from "./router";
  import { closeFocused } from "@app/components/Popover.svelte";
  import { httpdStore } from "@app/lib/httpd";
-
  import { isLocal } from "@app/lib/utils";
+
  import { baseUrlToUrl, isLocal } from "@app/lib/utils";
  import capitalize from "lodash/capitalize";

  import Button from "@app/components/Button.svelte";
@@ -174,7 +174,12 @@
  </List>

  {#if error}
-
    <ErrorMessage message="Couldn't load issues" {error} />
+
    <ErrorMessage
+
      title="Couldn't load issues"
+
      description="Please make sure you are able to connect to the seed <code>${baseUrlToUrl(
+
        api.baseUrl,
+
      ).toString()}</code>"
+
      {error} />
  {/if}

  {#if project.issues[state] === 0}
modified src/views/projects/Patches.svelte
@@ -6,7 +6,7 @@

  import { PATCHES_PER_PAGE } from "./router";
  import { httpdStore } from "@app/lib/httpd";
-
  import { isLocal } from "@app/lib/utils";
+
  import { baseUrlToUrl, isLocal } from "@app/lib/utils";

  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
@@ -193,7 +193,12 @@
  </List>

  {#if error}
-
    <ErrorMessage message="Couldn't load patches" {error} />
+
    <ErrorMessage
+
      title="Couldn't load patches"
+
      description="Please make sure you are able to connect to the seed <code>${baseUrlToUrl(
+
        api.baseUrl,
+
      ).toString()}</code>"
+
      {error} />
  {/if}

  {#if project.patches[state] === 0}
added src/views/projects/error.ts
@@ -0,0 +1,59 @@
+
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
+
import type { ProjectRoute } from "@app/views/projects/router";
+

+
import { baseUrlToUrl } from "@app/lib/utils";
+
import { ResponseParseError, ResponseError } from "@httpd-client/lib/fetcher";
+

+
export function handleError(
+
  error: Error | ResponseParseError | ResponseError,
+
  route: ProjectRoute,
+
): NotFoundRoute | ErrorRoute {
+
  const url = baseUrlToUrl(route.node);
+
  if (error instanceof ResponseError && error.status === 404) {
+
    let subject;
+

+
    if (route.resource === "project.commit") {
+
      subject = "Commit";
+
    } else if (route.resource === "project.issue") {
+
      subject = "Issue";
+
    } else if (route.resource === "project.patch") {
+
      subject = "Patch";
+
    } else {
+
      subject = "Project";
+
    }
+

+
    return {
+
      resource: "notFound",
+
      params: { title: `${subject} not found` },
+
    };
+
  } else if (error instanceof ResponseError) {
+
    return {
+
      resource: "error",
+
      params: {
+
        error,
+
        title: "Could not load this project",
+
        description: `Make sure you are able to connect to the seed <a href="${url}">${url}</a>.`,
+
      },
+
    };
+
  } else if (error instanceof ResponseParseError) {
+
    return {
+
      resource: "error",
+
      params: {
+
        error,
+
        title: "Could not parse the request",
+
        description:
+
          "The response received from the seed does not match the expected schema, this is usually due to a version mismatch between the seed and the web interface.",
+
      },
+
    };
+
  } else {
+
    return {
+
      resource: "error",
+
      params: {
+
        error,
+
        title: "Could not load this project",
+
        description:
+
          "You stumbled on an unknown error, we aren't exactly sure what happened.",
+
      },
+
    };
+
  }
+
}
modified src/views/projects/router.ts
@@ -1,7 +1,4 @@
-
import type {
-
  LoadErrorRoute,
-
  NotFoundRoute,
-
} from "@app/lib/router/definitions";
+
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type {
  BaseUrl,
  Blob,
@@ -21,9 +18,10 @@ import type {

import * as Syntax from "@app/lib/syntax";
import * as httpd from "@app/lib/httpd";
-
import { config } from "@app/lib/config";
import { HttpdClient } from "@httpd-client";
import { ResponseError } from "@httpd-client/lib/fetcher";
+
import { config } from "@app/lib/config";
+
import { handleError } from "@app/views/projects/error";
import { nodePath } from "@app/views/nodes/router";
import { unreachable } from "@app/lib/utils";

@@ -271,7 +269,7 @@ async function isLocalNodeSeeding(route: ProjectRoute): Promise<boolean> {

export async function loadProjectRoute(
  route: ProjectRoute,
-
): Promise<ProjectLoadedRoute | LoadErrorRoute | NotFoundRoute> {
+
): Promise<ProjectLoadedRoute | ErrorRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
    `${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
@@ -341,35 +339,7 @@ export async function loadProjectRoute(
      return unreachable(route);
    }
  } catch (error: any) {
-
    if (error?.status === 404) {
-
      let subject;
-

-
      if (route.resource === "project.commit") {
-
        subject = "Commit";
-
      } else if (route.resource === "project.issue") {
-
        subject = "Issue";
-
      } else if (route.resource === "project.patch") {
-
        subject = "Patch";
-
      } else {
-
        subject = "Project";
-
      }
-

-
      return {
-
        resource: "notFound",
-
        params: {
-
          title: `${subject} not found`,
-
        },
-
      };
-
    } else {
-
      return {
-
        resource: "loadError",
-
        params: {
-
          title: "Could not load this project",
-
          errorMessage: error.message,
-
          stackTrace: error.stack,
-
        },
-
      };
-
    }
+
    return handleError(error, route);
  }
}

modified tests/visual/desktop/landingPage.spec.ts
@@ -40,3 +40,26 @@ test("load error", async ({ page }) => {
  await page.goto("/", { waitUntil: "networkidle" });
  await expect(page).toHaveScreenshot();
});
+

+
test("response parse error", async ({ page }) => {
+
  await page.addInitScript(appConfigWithFixture);
+
  await page.route("*/**/v1/projects*", route => {
+
    return route.fulfill({
+
      json: [{ name: 1337 }],
+
    });
+
  });
+
  await page.goto("/", { waitUntil: "networkidle" });
+
  await expect(page).toHaveScreenshot();
+
});
+

+
test("response error", async ({ page }) => {
+
  await page.addInitScript(appConfigWithFixture);
+
  await page.route("*/**/v1/projects*", route => {
+
    return route.fulfill({
+
      status: 500,
+
      body: "There is an error in the response",
+
    });
+
  });
+
  await page.goto("/", { waitUntil: "networkidle" });
+
  await expect(page).toHaveScreenshot();
+
});
modified tests/visual/desktop/node.spec.ts
@@ -37,3 +37,29 @@ test("node not found", async ({ page }) => {
  });
  await expect(page).toHaveScreenshot();
});
+

+
test("response parse error", async ({ page }) => {
+
  await page.route("*/**/v1/projects*", route => {
+
    return route.fulfill({
+
      json: [{ name: 1337 }],
+
    });
+
  });
+

+
  await page.goto("/nodes/radicle.local", {
+
    waitUntil: "networkidle",
+
  });
+
  await expect(page).toHaveScreenshot();
+
});
+

+
test("response error", async ({ page }) => {
+
  await page.route("*/**/v1/projects*", route => {
+
    return route.fulfill({
+
      status: 500,
+
    });
+
  });
+

+
  await page.goto("/nodes/radicle.local", {
+
    waitUntil: "networkidle",
+
  });
+
  await expect(page).toHaveScreenshot();
+
});
modified tests/visual/desktop/project.spec.ts
@@ -5,6 +5,7 @@ import {
  sourceBrowsingUrl,
  aliceRemote,
  markdownUrl,
+
  sourceBrowsingRid,
} from "@tests/support/fixtures.js";

test("source page", async ({ page }) => {
@@ -99,6 +100,26 @@ test("project not found", async ({ page }) => {
  await expect(page).toHaveScreenshot();
});

+
test("response parse error", async ({ page }) => {
+
  await page.route(`*/**/v1/projects/${sourceBrowsingRid}`, route => {
+
    return route.fulfill({
+
      json: [{ name: 1337 }],
+
    });
+
  });
+
  await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
+
  await expect(page).toHaveScreenshot();
+
});
+

+
test("response error", async ({ page }) => {
+
  await page.route(`*/**/v1/projects/${sourceBrowsingRid}`, route => {
+
    return route.fulfill({
+
      status: 500,
+
    });
+
  });
+
  await page.goto(sourceBrowsingUrl, { waitUntil: "networkidle" });
+
  await expect(page).toHaveScreenshot();
+
});
+

test("readme not found", async ({ page }) => {
  await page.goto(`${markdownUrl}/tree`, {
    waitUntil: "networkidle",