fix: error handling improvements (#3797)

* fix: error handling improvements

* refactor: error info cards

* refactor: PyroError -> ModrinthError

* fix: lint

* fix: idiot

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
IMB11
2025-06-16 18:34:18 +01:00
committed by GitHub
parent 0a9ffd3dc8
commit 706976439d
7 changed files with 398 additions and 198 deletions

View File

@@ -124,58 +124,63 @@ export class ModrinthServer {
return dataURL; return dataURL;
} }
} catch (error) { } catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) { if (error instanceof ModrinthServerError && error.statusCode === 404) {
// Handle external icon processing if (iconUrl) {
try { try {
const response = await fetch(iconUrl); const response = await fetch(iconUrl);
if (!response.ok) throw new Error("Failed to fetch icon"); if (!response.ok) throw new Error("Failed to fetch icon");
const file = await response.blob(); const file = await response.blob();
const originalFile = new File([file], "server-icon-original.png", { const originalFile = new File([file], "server-icon-original.png", {
type: "image/png", type: "image/png",
});
if (import.meta.client) {
const dataURL = await new Promise<string>((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = 64;
canvas.height = 64;
ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob(async (blob) => {
if (blob) {
const scaledFile = new File([blob], "server-icon.png", { type: "image/png" });
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: scaledFile,
override: auth,
});
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: originalFile,
override: auth,
});
}
}, "image/png");
const dataURL = canvas.toDataURL("image/png");
sharedImage.value = dataURL;
resolve(dataURL);
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
}); });
return dataURL;
if (import.meta.client) {
const dataURL = await new Promise<string>((resolve) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = 64;
canvas.height = 64;
ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob(async (blob) => {
if (blob) {
const scaledFile = new File([blob], "server-icon.png", {
type: "image/png",
});
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: scaledFile,
override: auth,
});
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: originalFile,
override: auth,
});
}
}, "image/png");
const dataURL = canvas.toDataURL("image/png");
sharedImage.value = dataURL;
resolve(dataURL);
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(file);
});
return dataURL;
}
} catch (externalError: any) {
console.debug("Could not process external icon:", externalError.message);
} }
} catch (error) {
console.error("Failed to process external icon:", error);
} }
} else {
throw error;
} }
} }
} catch (error) { } catch (error: any) {
console.error("Failed to process server icon:", error); console.debug("Icon processing failed:", error.message);
} }
sharedImage.value = undefined; sharedImage.value = undefined;
@@ -239,6 +244,18 @@ export class ModrinthServer {
break; break;
} }
} catch (error) { } catch (error) {
if (error instanceof ModrinthServerError) {
if (error.statusCode === 404 && ["fs", "content"].includes(module)) {
console.debug(`Optional ${module} resource not found:`, error.message);
continue;
}
if (error.statusCode === 503) {
console.debug(`Temporary ${module} unavailable:`, error.message);
continue;
}
}
this.errors[module] = { this.errors[module] = {
error: error:
error instanceof ModrinthServerError error instanceof ModrinthServerError

View File

@@ -155,19 +155,25 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
} }
async setMotd(motd: string): Promise<void> { async setMotd(motd: string): Promise<void> {
const props = (await this.server.fetchConfigFile("ServerProperties")) as any; try {
if (props) { const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
props.motd = motd; if (props) {
const newProps = this.server.constructServerProperties(props); props.motd = motd;
const octetStream = new Blob([newProps], { type: "application/octet-stream" }); const newProps = this.server.constructServerProperties(props);
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`); const octetStream = new Blob([newProps], { type: "application/octet-stream" });
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
await useServersFetch(`/update?path=/server.properties`, { await useServersFetch(`/update?path=/server.properties`, {
method: "PUT", method: "PUT",
contentType: "application/octet-stream", contentType: "application/octet-stream",
body: octetStream, body: octetStream,
override: auth, override: auth,
}); });
}
} catch {
console.error(
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
);
} }
} }
} }

View File

@@ -18,48 +18,25 @@
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'" v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Server upgrading"
<div class="flex flex-col items-center gap-4"> description="Your server's hardware is currently being upgraded and will be back online shortly!"
<div class="grid place-content-center rounded-full bg-bg-blue p-4"> :icon="TransferIcon"
<TransferIcon class="size-12 text-blue" /> icon-color="blue"
</div> :action="generalErrorAction"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server upgrading</h1> />
</div>
<p class="text-lg text-secondary">
Your server's hardware is currently being upgraded and will be back online shortly!
</p>
</div>
</div>
</div> </div>
<div <div
v-else-if="serverData?.status === 'suspended'" v-else-if="serverData?.status === 'suspended'"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Server suspended"
<div class="flex flex-col items-center gap-4"> :description="suspendedDescription"
<div class="grid place-content-center rounded-full bg-bg-orange p-4"> :icon="LockIcon"
<LockIcon class="size-12 text-orange" /> icon-color="orange"
</div> :action="suspendedAction"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server suspended</h1> />
</div>
<p class="text-lg text-secondary">
{{
serverData.suspension_reason === "cancelled"
? "Your subscription has been cancelled."
: serverData.suspension_reason
? `Your server has been suspended: ${serverData.suspension_reason}`
: "Your server has been suspended."
}}
<br />
Contact Modrinth Support if you believe this is an error.
</p>
</div>
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
<button class="mt-6 !w-full">Go to billing settings</button>
</ButtonStyled>
</div>
</div> </div>
<div <div
v-else-if=" v-else-if="
@@ -68,110 +45,69 @@
" "
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="An error occured."
<div class="flex flex-col items-center gap-4"> description="Please contact Modrinth Support."
<div class="grid place-content-center rounded-full bg-bg-orange p-4"> :icon="TransferIcon"
<TransferIcon class="size-12 text-orange" /> icon-color="orange"
</div> :error-details="generalErrorDetails"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1> :action="generalErrorAction"
</div> />
<p class="text-lg text-secondary">
You don't have permission to view this server or it no longer exists. If you believe this
is an error, please contact Modrinth Support.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
<button class="mt-6 !w-full">Go back to all servers</button>
</ButtonStyled>
</div>
</div> </div>
<div <div
v-else-if="server.moduleErrors?.general?.error.statusCode === 503" v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Server Node Unavailable"
<div class="flex flex-col items-center gap-4"> :icon="PanelErrorIcon"
<div class="grid place-content-center rounded-full bg-bg-red p-4"> icon-color="red"
<UiServersIconsPanelErrorIcon class="size-12 text-red" /> :action="nodeUnavailableAction"
</div> :error-details="nodeUnavailableDetails"
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1> >
<template #description>
<div class="text-md space-y-4">
<p class="leading-[170%] text-secondary">
Your server's node, where your Modrinth Server is physically hosted, is experiencing
issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p>
<p class="leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as
the issue is resolved.
</p>
<p class="leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
</div> </div>
<p class="m-0 mb-4 leading-[170%] text-secondary"> </template>
Your server's node, where your Modrinth Server is physically hosted, is experiencing </ErrorInformationCard>
issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p>
<p class="m-0 mb-4 leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as the
issue is resolved.
</p>
<p class="m-0 mb-4 leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
<div class="flex flex-col gap-2">
<UiCopyCode :text="'Server ID: ' + server.serverId" />
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
</div>
</div>
<ButtonStyled
size="large"
color="standard"
@click="
() =>
navigateTo('https://discord.modrinth.com', {
external: true,
})
"
>
<button class="mt-6 !w-full">Join Modrinth Discord</button>
</ButtonStyled>
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
color="standard"
@click="() => reloadNuxtApp()"
>
<button class="mt-3 !w-full">Reload</button>
</ButtonStyled>
</div>
</div> </div>
<div <div
v-else-if="server.moduleErrors?.general?.error" v-else-if="server.moduleErrors?.general?.error"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
> >
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl"> <ErrorInformationCard
<div class="flex flex-col items-center text-center"> title="Connection lost"
<div class="flex flex-col items-center gap-4"> description=""
<div class="grid place-content-center rounded-full bg-bg-orange p-4"> :icon="TransferIcon"
<TransferIcon class="size-12 text-orange" /> icon-color="orange"
</div> :action="connectionLostAction"
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Connection lost</h1> >
<template #description>
<div class="space-y-4">
<div class="text-center text-secondary"> <div class="text-center text-secondary">
{{ {{
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...` formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
}} }}
</div> </div>
<p class="text-lg text-secondary">
Something went wrong, and we couldn't connect to your server. This is likely due to a
temporary network issue. You'll be reconnected automatically.
</p>
</div> </div>
<p class="text-lg text-secondary"> </template>
Something went wrong, and we couldn't connect to your server. This is likely due to a </ErrorInformationCard>
temporary network issue. You'll be reconnected automatically.
</p>
</div>
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
<ButtonStyled
:disabled="formattedTime !== '00'"
size="large"
color="brand"
@click="() => reloadNuxtApp()"
>
<button class="mt-6 !w-full">Reload</button>
</ButtonStyled>
</div>
</div> </div>
<!-- SERVER START --> <!-- SERVER START -->
<div <div
@@ -432,7 +368,7 @@ import {
LockIcon, LockIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { ButtonStyled, ServerNotice } from "@modrinth/ui"; import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
import { Intercom, shutdown } from "@intercom/messenger-js-sdk"; import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
import type { MessageDescriptor } from "@vintl/vintl"; import type { MessageDescriptor } from "@vintl/vintl";
import type { import type {
@@ -448,6 +384,7 @@ import { useModrinthServersConsole } from "~/store/console.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts"; import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts"; import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue"; import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
import PanelErrorIcon from "~/components/ui/servers/icons/PanelErrorIcon.vue";
const app = useNuxtApp() as unknown as { $notify: any }; const app = useNuxtApp() as unknown as { $notify: any };
@@ -760,7 +697,7 @@ const startUptimeUpdates = () => {
const stopUptimeUpdates = () => { const stopUptimeUpdates = () => {
if (uptimeIntervalId) { if (uptimeIntervalId) {
clearInterval(uptimeIntervalId); clearInterval(uptimeIntervalId);
intervalId = null; pollingIntervalId = null;
} }
}; };
@@ -1055,7 +992,7 @@ const notifyError = (title: string, text: string) => {
}); });
}; };
let intervalId: ReturnType<typeof setInterval> | null = null; let pollingIntervalId: ReturnType<typeof setInterval> | null = null;
const countdown = ref(15); const countdown = ref(15);
const formattedTime = computed(() => { const formattedTime = computed(() => {
@@ -1099,23 +1036,142 @@ const backupInProgress = computed(() => {
}); });
const stopPolling = () => { const stopPolling = () => {
if (intervalId) { if (pollingIntervalId) {
clearInterval(intervalId); clearTimeout(pollingIntervalId);
intervalId = null; pollingIntervalId = null;
} }
}; };
const startPolling = () => { const startPolling = () => {
countdown.value = 15; stopPolling();
intervalId = setInterval(() => {
if (countdown.value <= 0) { let retryCount = 0;
reloadNuxtApp(); const maxRetries = 10;
} else {
countdown.value--; const poll = async () => {
try {
await server.refresh(["general", "ws"]);
if (!server.moduleErrors?.general?.error) {
stopPolling();
connectWebSocket();
return;
}
retryCount++;
if (retryCount >= maxRetries) {
console.error("Max retries reached, stopping polling");
stopPolling();
return;
}
// Exponential backoff: 3s, 6s, 12s, 24s, etc.
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
} catch (error) {
console.error("Polling failed:", error);
retryCount++;
if (retryCount < maxRetries) {
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
}
} }
}, 1000); };
poll();
}; };
const nodeUnavailableDetails = computed(() => [
{
label: "Server ID",
value: server.serverId,
type: "inline" as const,
},
{
label: "Node",
value: server.general?.datacenter ?? "Unknown! Please contact support!",
type: "inline" as const,
},
]);
const suspendedDescription = computed(() => {
if (serverData.value?.suspension_reason === "cancelled") {
return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error.";
}
if (serverData.value?.suspension_reason) {
return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`;
}
return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error.";
});
const generalErrorDetails = computed(() => [
{
label: "Server ID",
value: server.serverId,
type: "inline" as const,
},
{
label: "Timestamp",
value: String(server.moduleErrors?.general?.timestamp),
type: "inline" as const,
},
{
label: "Error Name",
value: server.moduleErrors?.general?.error.name,
type: "inline" as const,
},
{
label: "Error Message",
value: server.moduleErrors?.general?.error.message,
type: "block" as const,
},
...(server.moduleErrors?.general?.error.originalError
? [
{
label: "Original Error",
value: String(server.moduleErrors.general.error.originalError),
type: "hidden" as const,
},
]
: []),
...(server.moduleErrors?.general?.error.stack
? [
{
label: "Stack Trace",
value: server.moduleErrors.general.error.stack,
type: "hidden" as const,
},
]
: []),
]);
const suspendedAction = computed(() => ({
label: "Go to billing settings",
onClick: () => router.push("/settings/billing"),
color: "brand" as const,
}));
const generalErrorAction = computed(() => ({
label: "Go back to all servers",
onClick: () => router.push("/servers/manage"),
color: "brand" as const,
}));
const nodeUnavailableAction = computed(() => ({
label: "Join Modrinth Discord",
onClick: () => navigateTo("https://discord.modrinth.com", { external: true }),
color: "standard" as const,
}));
const connectionLostAction = computed(() => ({
label: "Reload",
onClick: () => reloadNuxtApp(),
color: "brand" as const,
disabled: formattedTime.value !== "00",
}));
const copyServerDebugInfo = () => { const copyServerDebugInfo = () => {
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`; const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`;
navigator.clipboard.writeText(debugInfo); navigator.clipboard.writeText(debugInfo);

View File

@@ -0,0 +1,120 @@
<template>
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-8 shadow-xl">
<div class="flex flex-col items-center text-center">
<div class="flex flex-col items-center gap-4">
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
<component :is="icon" class="size-12 text-orange" />
</div>
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">{{ title }}</h1>
</div>
<div v-if="!description">
<slot name="description" />
</div>
<p v-else class="text-lg text-secondary">{{ description }}</p>
</div>
<div v-if="errorDetails" class="my-4 w-full rounded-lg border border-divider bg-bg-raised">
<div class="divide-y divide-divider">
<div
v-for="detail in errorDetails.filter((detail) => detail.type !== 'hidden')"
:key="detail.label"
class="px-4 py-3"
>
<div v-if="detail.type === 'inline'" class="flex items-center justify-between">
<span class="font-medium text-secondary">{{ detail.label }}</span>
<div class="flex items-center gap-2">
<code class="rounded-lg bg-code-bg px-2 py-1 text-sm text-code-text">
{{ detail.value }}
</code>
</div>
</div>
<div v-else-if="detail.type === 'block'" class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="font-medium text-secondary">{{ detail.label }}</span>
</div>
<div class="w-full overflow-hidden rounded-lg bg-code-bg p-3">
<code
class="block w-full overflow-x-auto break-words text-sm text-code-text whitespace-pre-wrap"
>
{{ detail.value }}
</code>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4 flex !w-full flex-row gap-4">
<ButtonStyled
v-if="action"
size="large"
:color="action.color || 'brand'"
:disabled="action.disabled"
@click="action.onClick"
>
<button class="!w-full">
<component :is="action.icon" v-if="action.icon && !action.showAltIcon" class="size-4" />
<component
:is="action.altIcon"
v-else-if="action.icon && action.showAltIcon"
class="size-4"
/>
{{ action.label }}
</button>
</ButtonStyled>
<ButtonStyled v-if="errorDetails" size="large" color="standard" @click="copyErrorInformation">
<button class="!w-full">
<CopyIcon v-if="!infoCopied" class="size-4" />
<CheckIcon v-else class="size-4" />
Copy Information
</button>
</ButtonStyled>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ButtonStyled from './ButtonStyled.vue'
import { CopyIcon, CheckIcon } from '@modrinth/assets'
import type { Component } from 'vue'
const infoCopied = ref(false)
const props = defineProps<{
title: string
description?: string
icon: Component
errorDetails?: {
label?: string
value?: string
type?: 'inline' | 'block' | 'hidden'
}[]
action?: {
label: string
onClick: () => void
color?: 'brand' | 'standard' | 'red' | 'orange' | 'blue'
disabled?: boolean
icon?: Component
altIcon?: Component
showAltIcon?: boolean
}
}>()
const copyErrorInformation = async () => {
if (!props.errorDetails || props.errorDetails.length === 0) return
const formattedErrorInfo = props.errorDetails
.filter((detail) => detail.label && detail.value)
.map((detail) => `${detail.label}: ${detail.value}`)
.join('\n\n')
await navigator.clipboard.writeText(formattedErrorInfo)
infoCopied.value = true
setTimeout(() => {
infoCopied.value = false
}, 2000)
}
</script>

View File

@@ -16,6 +16,7 @@ export { default as DoubleIcon } from './base/DoubleIcon.vue'
export { default as DropArea } from './base/DropArea.vue' export { default as DropArea } from './base/DropArea.vue'
export { default as DropdownSelect } from './base/DropdownSelect.vue' export { default as DropdownSelect } from './base/DropdownSelect.vue'
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue' export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue'
export { default as FileInput } from './base/FileInput.vue' export { default as FileInput } from './base/FileInput.vue'
export { default as FilterBar } from './base/FilterBar.vue' export { default as FilterBar } from './base/FilterBar.vue'
export type { FilterBarOption } from './base/FilterBar.vue' export type { FilterBarOption } from './base/FilterBar.vue'

View File

@@ -54,6 +54,6 @@ export class ModrinthServerError extends Error {
} }
super(errorMessage) super(errorMessage)
this.name = 'PyroServersFetchError' this.name = 'ModrinthServersFetchError'
} }
} }

View File

@@ -5,6 +5,6 @@ export class ModrinthServersFetchError extends Error {
public originalError?: Error, public originalError?: Error,
) { ) {
super(message) super(message)
this.name = 'PyroFetchError' this.name = 'ModrinthFetchError'
} }
} }