Revert "fix: error handling improvements (#3797)"

This reverts commit 706976439d.
This commit is contained in:
Prospector
2025-07-07 17:37:43 -07:00
parent e4e77dc0d2
commit e0cde2d6ff
7 changed files with 198 additions and 398 deletions

View File

@@ -124,8 +124,8 @@ export class ModrinthServer {
return dataURL; return dataURL;
} }
} catch (error) { } catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 404) { if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) {
if (iconUrl) { // Handle external icon processing
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");
@@ -145,9 +145,7 @@ export class ModrinthServer {
ctx?.drawImage(img, 0, 0, 64, 64); ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob(async (blob) => { canvas.toBlob(async (blob) => {
if (blob) { if (blob) {
const scaledFile = new File([blob], "server-icon.png", { const scaledFile = new File([blob], "server-icon.png", { type: "image/png" });
type: "image/png",
});
await useServersFetch(`/create?path=/server-icon.png&type=file`, { await useServersFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST", method: "POST",
contentType: "application/octet-stream", contentType: "application/octet-stream",
@@ -171,16 +169,13 @@ export class ModrinthServer {
}); });
return dataURL; return dataURL;
} }
} catch (externalError: any) { } catch (error) {
console.debug("Could not process external icon:", externalError.message); 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;
@@ -244,18 +239,6 @@ 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

@@ -194,7 +194,6 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
} }
async setMotd(motd: string): Promise<void> { async setMotd(motd: string): Promise<void> {
try {
const props = (await this.server.fetchConfigFile("ServerProperties")) as any; const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
if (props) { if (props) {
props.motd = motd; props.motd = motd;
@@ -209,10 +208,5 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
override: auth, override: auth,
}); });
} }
} catch {
console.error(
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
);
}
} }
} }

View File

@@ -18,25 +18,48 @@
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"
> >
<ErrorInformationCard <div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
title="Server upgrading" <div class="flex flex-col items-center text-center">
description="Your server's hardware is currently being upgraded and will be back online shortly!" <div class="flex flex-col items-center gap-4">
:icon="TransferIcon" <div class="grid place-content-center rounded-full bg-bg-blue p-4">
icon-color="blue" <TransferIcon class="size-12 text-blue" />
:action="generalErrorAction" </div>
/> <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"
> >
<ErrorInformationCard <div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
title="Server suspended" <div class="flex flex-col items-center text-center">
:description="suspendedDescription" <div class="flex flex-col items-center gap-4">
:icon="LockIcon" <div class="grid place-content-center rounded-full bg-bg-orange p-4">
icon-color="orange" <LockIcon class="size-12 text-orange" />
:action="suspendedAction" </div>
/> <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="
@@ -45,69 +68,110 @@
" "
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"
> >
<ErrorInformationCard <div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
title="An error occured." <div class="flex flex-col items-center text-center">
description="Please contact Modrinth Support." <div class="flex flex-col items-center gap-4">
:icon="TransferIcon" <div class="grid place-content-center rounded-full bg-bg-orange p-4">
icon-color="orange" <TransferIcon class="size-12 text-orange" />
:error-details="generalErrorDetails" </div>
:action="generalErrorAction" <h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1>
/> </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"
> >
<ErrorInformationCard <div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
title="Server Node Unavailable" <div class="flex flex-col items-center text-center">
:icon="PanelErrorIcon" <div class="flex flex-col items-center gap-4">
icon-color="red" <div class="grid place-content-center rounded-full bg-bg-red p-4">
:action="nodeUnavailableAction" <UiServersIconsPanelErrorIcon class="size-12 text-red" />
:error-details="nodeUnavailableDetails" </div>
> <h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1>
<template #description> </div>
<div class="text-md space-y-4"> <p class="m-0 mb-4 leading-[170%] text-secondary">
<p class="leading-[170%] text-secondary">
Your server's node, where your Modrinth Server is physically hosted, is experiencing 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. issues. We are working with our datacenter to resolve the issue as quickly as possible.
</p> </p>
<p class="leading-[170%] text-secondary"> <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 Your data is safe and will not be lost, and your server will be back online as soon as the
the issue is resolved. issue is resolved.
</p> </p>
<p class="leading-[170%] text-secondary"> <p class="m-0 mb-4 leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat 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. bubble in the bottom right corner and we'll be happy to help.
</p> </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>
</template>
</ErrorInformationCard>
</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"
> >
<ErrorInformationCard <div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
title="Connection lost" <div class="flex flex-col items-center text-center">
description="" <div class="flex flex-col items-center gap-4">
:icon="TransferIcon" <div class="grid place-content-center rounded-full bg-bg-orange p-4">
icon-color="orange" <TransferIcon class="size-12 text-orange" />
:action="connectionLostAction" </div>
> <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>
</div>
<p class="text-lg text-secondary"> <p class="text-lg text-secondary">
Something went wrong, and we couldn't connect to your server. This is likely due to a 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. temporary network issue. You'll be reconnected automatically.
</p> </p>
</div> </div>
</template> <UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
</ErrorInformationCard> <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
@@ -368,7 +432,7 @@ import {
LockIcon, LockIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui"; import { ButtonStyled, 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 {
@@ -384,7 +448,6 @@ 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 };
@@ -697,7 +760,7 @@ const startUptimeUpdates = () => {
const stopUptimeUpdates = () => { const stopUptimeUpdates = () => {
if (uptimeIntervalId) { if (uptimeIntervalId) {
clearInterval(uptimeIntervalId); clearInterval(uptimeIntervalId);
pollingIntervalId = null; intervalId = null;
} }
}; };
@@ -992,7 +1055,7 @@ const notifyError = (title: string, text: string) => {
}); });
}; };
let pollingIntervalId: ReturnType<typeof setInterval> | null = null; let intervalId: ReturnType<typeof setInterval> | null = null;
const countdown = ref(15); const countdown = ref(15);
const formattedTime = computed(() => { const formattedTime = computed(() => {
@@ -1036,142 +1099,23 @@ const backupInProgress = computed(() => {
}); });
const stopPolling = () => { const stopPolling = () => {
if (pollingIntervalId) { if (intervalId) {
clearTimeout(pollingIntervalId); clearInterval(intervalId);
pollingIntervalId = null; intervalId = null;
} }
}; };
const startPolling = () => { const startPolling = () => {
stopPolling(); countdown.value = 15;
intervalId = setInterval(() => {
let retryCount = 0; if (countdown.value <= 0) {
const maxRetries = 10; reloadNuxtApp();
} else {
const poll = async () => { countdown.value--;
try {
await server.refresh(["general", "ws"]);
if (!server.moduleErrors?.general?.error) {
stopPolling();
connectWebSocket();
return;
} }
}, 1000);
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);
}
}
};
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

@@ -1,120 +0,0 @@
<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,7 +16,6 @@ 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 = 'ModrinthServersFetchError' this.name = 'PyroServersFetchError'
} }
} }

View File

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