Skip to content

Commit

Permalink
Demonstrate deferring server-side errors
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye committed Oct 30, 2023
1 parent b8a31f7 commit ede88cb
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 13 deletions.
1 change: 1 addition & 0 deletions frontend/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare global {
interface Locals {
client: Client;
getUser: (() => LexAuthUser | null);
deferError: (error: unknown) => void;
}

interface Error {
Expand Down
20 changes: 16 additions & 4 deletions frontend/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ function getRoot(routeId: string): string {
}

// eslint-disable-next-line func-style
export const handle: Handle = ({ event, resolve }) => {
export const handle: Handle = async ({ event, resolve }) => {
const deferredErrors: unknown[] = [];
event.locals.deferError = (error) => deferredErrors.push(error);
console.log(`HTTP request: ${event.request.method} ${event.request.url}`);
event.locals.getUser = () => getUser(event.cookies);
return traceRequest(event, async () => {
return traceRequest(event, async (span) => {
await loadI18n();

const options: ResolveOptions = {
Expand All @@ -37,8 +39,18 @@ export const handle: Handle = ({ event, resolve }) => {
throw redirect(307, '/login');
}

return resolve(event, options);
})
const response = await resolve(event, options);

if (deferredErrors.length) {
deferredErrors.forEach((error) =>
ensureErrorIsTraced(error, { event, span }, {
['app.error.source']: 'server-handle-hook',
}));
throw deferredErrors[0];
}

return response;
});
};

// eslint-disable-next-line func-style
Expand Down
28 changes: 21 additions & 7 deletions frontend/src/lib/gql/gql-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ function createGqlClient(_gqlEndpoint?: string): Client {
});
}

export function getClient(): GqlClient {
export function getClient(deferError?: (error: unknown) => void): GqlClient {
if (browser) {
if (globalClient) return globalClient;
globalClient = new GqlClient(createGqlClient(''));
return globalClient;
} else {
//We do not cache the client on the server side.
return new GqlClient(createGqlClient());
return new GqlClient(createGqlClient(), deferError);
}
}

Expand All @@ -74,7 +74,7 @@ type QueryStoreReturnType<Data> = { [K in keyof Data]: Readable<Data[K]> };

class GqlClient {
public ownedByUserId = '';
constructor(public readonly client: Client) {
constructor(public readonly client: Client, private readonly deferError?: (error: unknown) => void) {
this.subscription = (...args) => this.client.subscription(...args);
}

Expand All @@ -90,12 +90,25 @@ class GqlClient {
query: TypedDocumentNode<Data, Variables>,
variables: Variables,
context: QueryOperationOptions = {}): OperationResultStore<Data, Variables> {
return queryStore<Data, Variables>({
const resultStore = queryStore<Data, Variables>({
client: this.client,
query,
variables,
context: {fetch, ...context}
});

return derived(resultStore, (result) => {
try {
this.throwAnyUnexpectedErrors(result);
} catch (error) {
if (browser) throw error;
else if (this.deferError) this.deferError(error);
else {
console.error(error); // throwing errors here kills vite, so this is the best we can do
}
}
return result;
});
}

async awaitedQueryStore<Data = unknown, Variables extends AnyVariables = AnyVariables>(
Expand Down Expand Up @@ -152,13 +165,14 @@ class GqlClient {
private throwAnyUnexpectedErrors<T extends OperationResult<unknown, AnyVariables>>(result: T): void {
const error = result.error;
if (!error) return;
if (this.is401(error)) throw redirect(307, '/logout');
if (this.isStatusCode(error, 401)) throw redirect(307, '/logout');
if (this.isStatusCode(error, 403)) throw redirect(307, '/home');
if (error.networkError) throw error.networkError; // e.g. SvelteKit redirects
throw error;
}

private is401(error: CombinedError): boolean {
return (error.response as Response | undefined)?.status === 401;
private isStatusCode(error: CombinedError, code: number): boolean {
return (error.response as Response | undefined)?.status === code;
}

private findInputErrors<T extends GenericData>({data}: OperationResult<T, AnyVariables>): LexGqlError<ExtractErrorTypename<T>> | undefined {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/otel/otel.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function tracer(): Tracer {
return trace.getTracer(SERVICE_NAME);
}

type ErrorTracer = ErrorHandler | 'server-gql-error' | 'client-gql-error' | 'client-fetch-error';
type ErrorTracer = ErrorHandler | 'server-gql-error' | 'client-gql-error' | 'client-fetch-error' | 'server-handle-hook';
type ErrorAttributes = Attributes & { ['app.error.source']: ErrorTracer };

interface ErrorContext {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';

export const load = ((event) => {
return {
deferError: event.locals.deferError,
};
}) satisfies PageServerLoad
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Project = NonNullable<ProjectPageQuery['projectByCode']>;
export type ProjectUser = Project['users'][number];

export async function load(event: PageLoadEvent) {
const client = getClient();
const client = getClient(event.data.deferError);
const projectCode = event.params.project_code;
const projectResult = await client
.awaitedQueryStore(event.fetch,
Expand Down

0 comments on commit ede88cb

Please sign in to comment.