Skip to content

Commit

Permalink
Standardize global fetch response validation on server and client
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye committed Oct 31, 2023
1 parent d0f98f8 commit 959f1b1
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 23 deletions.
22 changes: 9 additions & 13 deletions frontend/src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ensureErrorIsTraced, traceFetch } from '$lib/otel/otel.client';
import { getErrorMessage, validateFetchResponse } from './hooks.shared';

import { redirect, type HandleClientError } from '@sveltejs/kit';
import { getErrorMessage } from './hooks.shared';
import { loadI18n } from '$lib/i18n';
import { APP_VERSION } from '$lib/util/version';
import type { HandleClientError } from '@sveltejs/kit';
import { USER_LOAD_KEY } from '$lib/user';
import { handleFetch } from '$lib/util/fetch-proxy';
import {invalidate} from '$app/navigation';
import {USER_LOAD_KEY} from '$lib/user';
import { invalidate } from '$app/navigation';
import { loadI18n } from '$lib/i18n';
import { updated } from '$app/stores';
import { APP_VERSION } from '$lib/util/version';

await loadI18n();

Expand Down Expand Up @@ -64,13 +64,9 @@ function shouldTryAutoReload(updateDetected: boolean): boolean {
handleFetch(async ({ fetch, args }) => {
const response = await traceFetch(() => fetch(...args));

if (response.status === 401 && location.pathname !== '/login') {
throw redirect(307, '/logout');
}

if (response.status >= 500) {
throw new Error(`Unexpected response: ${response.statusText} (${response.status}). URL: ${response.url}.`);
}
validateFetchResponse(response,
location.pathname === '/login',
location.pathname === '/' || location.pathname === '/home' || location.pathname === '/admin');

if (response.headers.get('lexbox-refresh-jwt') == 'true') {
await invalidate(USER_LOAD_KEY);
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import { redirect, type Handle, type HandleFetch, type HandleServerError, type R
import { loadI18n } from '$lib/i18n';
import { ensureErrorIsTraced, traceRequest, traceFetch } from '$lib/otel/otel.server'
import { env } from '$env/dynamic/private';
import { getErrorMessage } from './hooks.shared';
import { getErrorMessage, validateFetchResponse } from './hooks.shared';

const UNAUTHENTICATED_ROOT = '(unauthenticated)';
const AUTHENTICATED_ROOT = '(authenticated)';

const PUBLIC_ROUTE_ROOTS = [
'(unauthenticated)',
UNAUTHENTICATED_ROOT,
'email',
'healthz',
];
Expand Down Expand Up @@ -55,6 +58,12 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
if (response.headers.has('lexbox-version')) {
apiVersion.value = response.headers.get('lexbox-version');
}

const routeId = event.route.id ?? '';
validateFetchResponse(response,
routeId.endsWith('/login'),
routeId.endsWith(AUTHENTICATED_ROOT) || routeId.endsWith('/home') || routeId.endsWith('/admin'));

return response;
};

Expand Down
22 changes: 22 additions & 0 deletions frontend/src/hooks.shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { redirect } from '@sveltejs/kit';

const sayWuuuuuuut = 'We\'re not sure what happened.';

export function getErrorMessage(error: unknown): string {
Expand All @@ -17,3 +19,23 @@ export function getErrorMessage(error: unknown): string {
sayWuuuuuuut
);
}

export function validateFetchResponse(response: Response, isAtLogin: boolean, isHome: boolean): void {
if (response.status === 401 && !isAtLogin) {
throw redirect(307, '/logout');
}

if (response.status === 403) {
if (isHome) {
// the user's JWT appears to be invalid
throw redirect(307, '/logout');
} else {
// the user tried to access something they don't have permission for
throw redirect(307, '/home');
}
}

if (response.status >= 500) {
throw new Error(`Unexpected response: ${response.statusText} (${response.status}). URL: ${response.url}.`);
}
}
11 changes: 3 additions & 8 deletions frontend/src/lib/gql/gql-client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import {redirect} from '@sveltejs/kit';
import {
type Client,
type AnyVariables,
type TypedDocumentNode,
type OperationContext,
type OperationResult,
fetchExchange,
type CombinedError,
queryStore,
type OperationResultSource,
type OperationResultStore
Expand Down Expand Up @@ -167,15 +165,12 @@ 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 (error.networkError) throw error.networkError; // e.g. SvelteKit redirects
// unexpected status codes are handled in the fetch hooks
// throws there (e.g. SvelteKit redirects) turn into networkErrors that we rethrow here
if (error.networkError) throw error.networkError;
throw error;
}

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

private findInputErrors<T extends GenericData>({data}: OperationResult<T, AnyVariables>): LexGqlError<ExtractErrorTypename<T>> | undefined {
const errors: GqlInputError<ExtractErrorTypename<T>>[] = [];
if (isObject(data)) {
Expand Down

0 comments on commit 959f1b1

Please sign in to comment.