From cd94cc9736d6b6c12ccbbcfe88cb6e9e1d893e75 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 4 Sep 2024 14:23:11 +0100 Subject: [PATCH] Added some better error handling for the electric sync --- .../webapp/app/routes/sync.traces.$traceId.ts | 91 ++++++++++-------- .../app/routes/sync.traces.runs.$traceId.ts | 93 +++++++++++-------- apps/webapp/app/utils/longPollingFetch.ts | 48 +++++++--- 3 files changed, 144 insertions(+), 88 deletions(-) diff --git a/apps/webapp/app/routes/sync.traces.$traceId.ts b/apps/webapp/app/routes/sync.traces.$traceId.ts index f28e3f791..955c8e1b8 100644 --- a/apps/webapp/app/routes/sync.traces.$traceId.ts +++ b/apps/webapp/app/routes/sync.traces.$traceId.ts @@ -6,45 +6,60 @@ import { getUserId } from "~/services/session.server"; import { longPollingFetch } from "~/utils/longPollingFetch"; export async function loader({ params, request }: LoaderFunctionArgs) { - const userId = await getUserId(request); + try { + const userId = await getUserId(request); - logger.log(`/sync/traces/${params.traceId}`, { userId }); + logger.log(`/sync/traces/${params.traceId}`, { userId }); - if (!userId) { - return new Response("No user found in cookie", { status: 401 }); + if (!userId) { + return new Response("No user found in cookie", { status: 401 }); + } + + const trace = await $replica.taskEvent.findFirst({ + select: { + organizationId: true, + }, + where: { + traceId: params.traceId, + }, + }); + + if (!trace) { + return new Response("No trace found", { status: 404 }); + } + + const member = await $replica.orgMember.findFirst({ + where: { + organizationId: trace.organizationId, + userId, + }, + }); + + if (!member) { + return new Response("Not a member of this org", { status: 401 }); + } + + const url = new URL(request.url); + const originUrl = new URL(`${env.ELECTRIC_ORIGIN}/v1/shape/public."TaskEvent"`); + url.searchParams.forEach((value, key) => { + originUrl.searchParams.set(key, value); + }); + + originUrl.searchParams.set("where", `"traceId"='${params.traceId}'`); + + return longPollingFetch(originUrl.toString()); + } catch (error) { + if (error instanceof Response) { + // Error responses from longPollingFetch + return error; + } else if (error instanceof TypeError) { + // Unexpected errors + logger.error("Unexpected error in loader:", { error: error.message }); + return new Response("An unexpected error occurred", { status: 500 }); + } else { + // Unknown errors + logger.error("Unknown error occurred in loader, not Error", { error: JSON.stringify(error) }); + return new Response("An unknown error occurred", { status: 500 }); + } } - - const trace = await $replica.taskEvent.findFirst({ - select: { - organizationId: true, - }, - where: { - traceId: params.traceId, - }, - }); - - if (!trace) { - return new Response("No trace found", { status: 404 }); - } - - const member = await $replica.orgMember.findFirst({ - where: { - organizationId: trace.organizationId, - userId, - }, - }); - - if (!member) { - return new Response("Not a member of this org", { status: 401 }); - } - - const url = new URL(request.url); - const originUrl = new URL(`${env.ELECTRIC_ORIGIN}/v1/shape/public."TaskEvent"`); - url.searchParams.forEach((value, key) => { - originUrl.searchParams.set(key, value); - }); - - originUrl.searchParams.set("where", `"traceId"='${params.traceId}'`); - - return longPollingFetch(originUrl.toString()); } diff --git a/apps/webapp/app/routes/sync.traces.runs.$traceId.ts b/apps/webapp/app/routes/sync.traces.runs.$traceId.ts index ae4f0b57d..d44ce1f88 100644 --- a/apps/webapp/app/routes/sync.traces.runs.$traceId.ts +++ b/apps/webapp/app/routes/sync.traces.runs.$traceId.ts @@ -11,50 +11,65 @@ const Params = z.object({ }); export async function loader({ params, request }: LoaderFunctionArgs) { - const userId = await getUserId(request); - const { traceId } = Params.parse(params); + try { + const userId = await getUserId(request); + const { traceId } = Params.parse(params); - logger.log(`/sync/runs/${traceId}`, { userId }); + logger.log(`/sync/runs/${traceId}`, { userId }); - if (!userId) { - return new Response("No user found in cookie", { status: 401 }); - } + if (!userId) { + return new Response("No user found in cookie", { status: 401 }); + } - const run = await $replica.taskRun.findFirst({ - select: { - project: { - select: { - organizationId: true, + const run = await $replica.taskRun.findFirst({ + select: { + project: { + select: { + organizationId: true, + }, }, }, - }, - where: { - traceId, - }, - }); + where: { + traceId, + }, + }); - if (!run) { - return new Response("No run found", { status: 404 }); + if (!run) { + return new Response("No run found", { status: 404 }); + } + + const member = await $replica.orgMember.findFirst({ + where: { + organizationId: run.project.organizationId, + userId, + }, + }); + + if (!member) { + return new Response("Not a member of this org", { status: 401 }); + } + + const url = new URL(request.url); + const originUrl = new URL(`${env.ELECTRIC_ORIGIN}/v1/shape/public."TaskRun"`); + url.searchParams.forEach((value, key) => { + originUrl.searchParams.set(key, value); + }); + + originUrl.searchParams.set("where", `"traceId"='${traceId}'`); + + return longPollingFetch(originUrl.toString()); + } catch (error) { + if (error instanceof Response) { + // Error responses from longPollingFetch + return error; + } else if (error instanceof TypeError) { + // Unexpected errors + logger.error("Unexpected error in loader:", { error: error.message }); + return new Response("An unexpected error occurred", { status: 500 }); + } else { + // Unknown errors + logger.error("Unknown error occurred in loader, not Error", { error: JSON.stringify(error) }); + return new Response("An unknown error occurred", { status: 500 }); + } } - - const member = await $replica.orgMember.findFirst({ - where: { - organizationId: run.project.organizationId, - userId, - }, - }); - - if (!member) { - return new Response("Not a member of this org", { status: 401 }); - } - - const url = new URL(request.url); - const originUrl = new URL(`${env.ELECTRIC_ORIGIN}/v1/shape/public."TaskRun"`); - url.searchParams.forEach((value, key) => { - originUrl.searchParams.set(key, value); - }); - - originUrl.searchParams.set("where", `"traceId"='${traceId}'`); - - return longPollingFetch(originUrl.toString()); } diff --git a/apps/webapp/app/utils/longPollingFetch.ts b/apps/webapp/app/utils/longPollingFetch.ts index d693f5234..94ea0851d 100644 --- a/apps/webapp/app/utils/longPollingFetch.ts +++ b/apps/webapp/app/utils/longPollingFetch.ts @@ -2,18 +2,44 @@ // erroneously (saying the body is gzipped when it's not) so we'll just remove // them to avoid content decoding errors in the browser. // + +import { logger } from "~/services/logger.server"; + // Similar-ish problem to https://github.com/wintercg/fetch/issues/23 export async function longPollingFetch(url: string, options?: RequestInit) { - let response = await fetch(url, options); - if (response.headers.get(`content-encoding`)) { - const headers = new Headers(response.headers); - headers.delete(`content-encoding`); - headers.delete(`content-length`); - response = new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers, - }); + try { + let response = await fetch(url, options); + + // Check if the response is ok (status in the range 200-299) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (response.headers.get(`content-encoding`)) { + const headers = new Headers(response.headers); + headers.delete(`content-encoding`); + headers.delete(`content-length`); + response = new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); + } + + return response; + } catch (error) { + if (error instanceof TypeError) { + // Network error or other fetch-related errors + logger.error("Network error:", { error: error.message }); + throw new Response("Network error occurred", { status: 503 }); + } else if (error instanceof Error) { + // HTTP errors or other known errors + logger.error("Fetch error:", { error: error.message }); + throw new Response(error.message, { status: 500 }); + } else { + // Unknown errors + logger.error("Unknown error occurred during fetch"); + throw new Response("An unknown error occurred", { status: 500 }); + } } - return response; }