diff --git a/frontend/src/app/(navfooter)/error.tsx b/frontend/src/app/(navfooter)/error.tsx
index 1f120aa9..2cfae610 100644
--- a/frontend/src/app/(navfooter)/error.tsx
+++ b/frontend/src/app/(navfooter)/error.tsx
@@ -2,6 +2,6 @@
"use client"
-import Error from "../error"
+import ErrorPage from "../error"
-export default Error
+export default ErrorPage
diff --git a/frontend/src/app/error.tsx b/frontend/src/app/error.tsx
index 38956884..537969b7 100644
--- a/frontend/src/app/error.tsx
+++ b/frontend/src/app/error.tsx
@@ -7,22 +7,36 @@ import { SyncIcon } from "@primer/octicons-react"
import Button from "@/components/Button"
import ErrorBoundary from "@/components/ErrorBoundary"
import SetPageTitle from "@/components/SetPageTitle"
+import { RequestFailedError } from "@/lib/api"
-export const metadata = {
- title: "Error",
+type ErrorPageProps = {error: Error, reset: () => void };
+
+function NetworkErrorPage({ error, reset }: ErrorPageProps) {
+ return <>
+
+
+
+ We're having some trouble reaching the backend
+
+
+ {error.toString()}
+
+
+
+ If your internet connection is okay, we're probably down for maintenance, and will be back shortly. If this issue persists - let us know.
+
+
+
+
+
+
+
+ >
}
-export default function Error({
- error,
- reset,
-}: {
- error: Error
- reset: () => void
-}) {
- useEffect(() => {
- console.error(error)
- }, [error])
-
+function UnexpectedErrorPage({ error, reset }: ErrorPageProps) {
return <>
@@ -46,3 +60,15 @@ export default function Error({
>
}
+
+export default function ErrorPage({ error, reset }: ErrorPageProps) {
+ useEffect(() => {
+ console.error(error)
+ }, [error])
+
+ return error instanceof RequestFailedError ? :
+}
+
+export const metadata = {
+ title: "Error",
+}
diff --git a/frontend/src/app/scratch/[slug]/ScratchEditor.tsx b/frontend/src/app/scratch/[slug]/ScratchEditor.tsx
index 8fa6522e..3ce7c0b7 100644
--- a/frontend/src/app/scratch/[slug]/ScratchEditor.tsx
+++ b/frontend/src/app/scratch/[slug]/ScratchEditor.tsx
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"
-import useSWR from "swr"
+import useSWR, { Middleware, SWRConfig } from "swr"
import Scratch from "@/components/Scratch"
import useWarnBeforeScratchUnload from "@/components/Scratch/hooks/useWarnBeforeScratchUnload"
@@ -18,13 +18,7 @@ function ScratchPageTitle({ scratch }: { scratch: api.Scratch }) {
return
}
-export interface Props {
- initialScratch: api.Scratch
- parentScratch?: api.Scratch
- initialCompilation?: api.Compilation
-}
-
-export default function ScratchEditor({ initialScratch, parentScratch, initialCompilation }: Props) {
+function ScratchEditorInner({ initialScratch, parentScratch, initialCompilation, offline }: Props) {
const [scratch, setScratch] = useState(initialScratch)
useWarnBeforeScratchUnload(scratch)
@@ -75,7 +69,42 @@ export default function ScratchEditor({ initialScratch, parentScratch, initialCo
return { ...scratch, ...partial }
})
}}
+ offline={offline}
/>
>
}
+
+export interface Props {
+ initialScratch: api.Scratch
+ parentScratch?: api.Scratch
+ initialCompilation?: api.Compilation
+ offline?: boolean
+}
+
+export default function ScratchEditor(props: Props) {
+ const [offline, setOffline] = useState(false)
+
+ const offlineMiddleware: Middleware = _useSWRNext => {
+ return (key, fetcher, config) => {
+ let swr = _useSWRNext(key, fetcher, config)
+
+ if (swr.error instanceof api.RequestFailedError) {
+ setOffline(true)
+ swr = Object.assign({}, swr, { error: null })
+ }
+
+ return swr
+ }
+ }
+
+ const onSuccess = () => {
+ setOffline(false)
+ }
+
+ return <>
+
+
+
+ >
+}
diff --git a/frontend/src/components/Scratch/Scratch.tsx b/frontend/src/components/Scratch/Scratch.tsx
index dc3045d7..63b8cc0d 100644
--- a/frontend/src/components/Scratch/Scratch.tsx
+++ b/frontend/src/components/Scratch/Scratch.tsx
@@ -116,6 +116,7 @@ export type Props = {
onChange: (scratch: Partial) => void
parentScratch?: api.Scratch
initialCompilation?: Readonly
+ offline: boolean
}
export default function Scratch({
@@ -123,6 +124,7 @@ export default function Scratch({
onChange,
parentScratch,
initialCompilation,
+ offline,
}: Props) {
const container = useSize()
const [layout, setLayout] = useState(undefined)
@@ -286,6 +288,15 @@ export default function Scratch({
}
}
+ const offlineOverlay = (
+ offline ? <>
+
+
The scratch editor is in offline mode. We're attempting to reconnect to the backend - as long as this tab is open, your work is safe.
+
+ >
+ : <>>
+ )
+
return
@@ -306,5 +317,6 @@ export default function Scratch({
renderTab={renderTab}
/>}
+ {offlineOverlay}
}
diff --git a/frontend/src/lib/api/request.ts b/frontend/src/lib/api/request.ts
index c5eccb0d..24e6382b 100644
--- a/frontend/src/lib/api/request.ts
+++ b/frontend/src/lib/api/request.ts
@@ -33,6 +33,13 @@ export class ResponseError extends Error {
}
}
+export class RequestFailedError extends Error {
+ constructor(message: string, url: string) {
+ super(`${message} (occured when fetching ${url})`)
+ this.name = "RequestFailedError"
+ }
+}
+
export function normalizeUrl(url: string) {
if (url.startsWith("/")) {
url = API_BASE + url
@@ -40,22 +47,36 @@ export function normalizeUrl(url: string) {
return url
}
-export async function get(url: string) {
+export async function errorHandledFetchJson(url: string, init?: RequestInit) {
+ let response: Response
+
url = normalizeUrl(url)
- console.info("GET", url)
+ try {
+ response = await fetch(url, init)
+ } catch (error) {
+ if (error instanceof TypeError) {
+ throw new RequestFailedError(error.message, url)
+ }
- const response = await fetch(url, {
- ...commonOpts,
- cache: "no-cache",
- next: { revalidate: 10 },
- })
-
- if (!response.ok) {
- throw new ResponseError(response, await response.json())
+ throw error
}
try {
+ if (response.status == 502) {
+ // We've received a "Gateway Unavailable" message from nginx.
+ // The backend's down.
+ throw new RequestFailedError("Backend gateway unavailable", url)
+ }
+
+ if (!response.ok) {
+ throw new ResponseError(response, await response.json())
+ }
+
+ if (response.status == 204) {
+ return null
+ }
+
return await response.json()
} catch (error) {
if (error instanceof SyntaxError) {
@@ -69,10 +90,17 @@ export async function get(url: string) {
}
}
-export async function post(url: string, data: Json | FormData, method = "POST") {
- url = normalizeUrl(url)
+export async function get(url: string) {
+ console.info("GET", normalizeUrl(url))
+ return await errorHandledFetchJson(url, {
+ ...commonOpts,
+ cache: "no-cache",
+ next: { revalidate: 10 },
+ })
+}
- console.info(method, url, data)
+export async function post(url: string, data: Json | FormData, method = "POST") {
+ console.info(method, normalizeUrl(url), data)
let body: string | FormData
if (data instanceof FormData) {
@@ -81,7 +109,7 @@ export async function post(url: string, data: Json | FormData, method = "POST")
body = JSON.stringify(data)
}
- const response = await fetch(url, {
+ return await errorHandledFetchJson(url, {
...commonOpts,
method,
body,
@@ -89,16 +117,6 @@ export async function post(url: string, data: Json | FormData, method = "POST")
"Content-Type": "application/json",
},
})
-
- if (!response.ok) {
- throw new ResponseError(response, await response.json())
- }
-
- if (response.status == 204) {
- return null
- } else {
- return await response.json()
- }
}
export async function patch(url: string, data: Json | FormData) {