Better RetryErrors

This commit is contained in:
Stan Wohlwend 2024-07-27 13:24:42 -07:00
parent 1128e59b6c
commit 3bebd3f4d1
7 changed files with 131 additions and 28 deletions

View File

@ -0,0 +1,14 @@
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {
return Response.json({
status: "ok",
}, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Expose-Headers": "*",
},
});
}

View File

@ -13,7 +13,7 @@ const corsAllowedRequestHeaders = [
'x-stack-client-version',
// Project auth
'x-stack-access-type',
//'x-stack-access-type',
'x-stack-publishable-client-key',
'x-stack-secret-server-key',
'x-stack-super-secret-admin-key',
@ -23,7 +23,7 @@ const corsAllowedRequestHeaders = [
'x-stack-refresh-token',
'x-stack-access-token',
// Others
// Sentry
'baggage',
'sentry-trace',
];
@ -40,17 +40,15 @@ export async function middleware(request: NextRequest) {
const isApiRequest = url.pathname.startsWith('/api/');
// default headers
const responseInit: ResponseInit = {
const responseInit: ResponseInit | undefined = isApiRequest ? {
headers: {
// CORS headers
...!isApiRequest ? {} : {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '),
"Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '),
},
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": corsAllowedRequestHeaders.join(', '),
"Access-Control-Expose-Headers": corsAllowedResponseHeaders.join(', '),
},
};
} : undefined;
// we want to allow preflight requests to pass through
// even if the API route does not implement OPTIONS

View File

@ -0,0 +1,14 @@
import { NextRequest } from "next/server";
export async function GET(req: NextRequest) {
return Response.json({
status: "ok",
}, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Expose-Headers": "*",
},
});
}

View File

@ -5,11 +5,13 @@ import { PostHogProvider } from 'posthog-js/react';
import { Suspense, useEffect, useState } from 'react';
if (typeof window !== 'undefined') {
posthog.init("phc_vIUFi0HzHo7oV26OsaZbUASqxvs8qOmap1UBYAutU4k", {
api_host: "/consume",
ui_host: "https://eu.i.posthog.com",
person_profiles: 'identified_only',
});
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "phc_vIUFi0HzHo7oV26OsaZbUASqxvs8qOmap1UBYAutU4k";
if (postHogKey.length > 5) {
posthog.init(postHogKey, {
api_host: "/consume",
ui_host: "https://eu.i.posthog.com",
});
}
}
export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
@ -27,7 +29,7 @@ function UserIdentityInner() {
if (user && user.id !== lastUserId) {
posthog.identify(user.id, {
primaryEmail: user.primaryEmail,
displayName: user.displayName,
displayName: user.displayName ?? user.primaryEmail ?? user.id,
});
setLastUserId(user.id);
} else if (!user && lastUserId) {

View File

@ -59,13 +59,18 @@ Middleware can be used whenever it is easy to tell whether a page should be prot
<Tab title="Middleware">
```tsx title="middleware.tsx"
export async function middleware(request: NextRequest) {
// You can add your own route protection logic here
const user = await stackServerApp.getUser();
if (!user) {
return NextResponse.redirect(new URL('/handler/sign-in', request.url));
}
return NextResponse.next();
}
export const config = {
// You can add your own route protection logic here
// Make sure not to protect the root URL, as it would prevent users from accessing static Next.js files or Stack's /handler path
matcher: '/protected/:path*',
};
```
</Tab>
@ -259,4 +264,4 @@ For more examples on how to use the `User` object, check the [CurrentUser object
## Next steps
In the next guide, we will show you how to put [your application into production](./production.mdx).
In the next guide, we will show you how to put [your application into production](./production.mdx).

View File

@ -8,6 +8,7 @@ import { StackAssertionError, throwErr } from '../utils/errors';
import { globalVar } from '../utils/globals';
import { ReadonlyJson } from '../utils/json';
import { Result } from "../utils/results";
import { deindent } from '../utils/strings';
import { CurrentUserCrud } from './crud/current-user';
import { ProviderAccessTokenCrud } from './crud/oauth';
import { InternalProjectsCrud, ProjectsCrud } from './crud/projects';
@ -37,6 +38,54 @@ export class StackClientInterface {
return this.options.baseUrl + "/api/v1";
}
public async runNetworkDiagnostics(session?: InternalSession | null, requestType?: "client" | "server" | "admin") {
const tryRequest = async (cb: () => Promise<void>) => {
try {
await cb();
return "OK";
} catch (e) {
return `${e}`;
}
};
const cfTrace = await tryRequest(async () => {
const res = await fetch("https://1.1.1.1/cdn-cgi/trace");
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
});
const apiRoot = session !== undefined && requestType !== undefined ? await tryRequest(async () => {
const res = await this.sendClientRequestInner("/", {}, session!, requestType);
if (res.status === "error") {
throw res.error;
}
}) : "Not tested";
const baseUrlBackend = await tryRequest(async () => {
const res = await fetch(new URL("/health", this.getApiUrl()));
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
});
const prodDashboard = await tryRequest(async () => {
const res = await fetch("https://app.stackframe.com/health");
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
});
const prodBackend = await tryRequest(async () => {
const res = await fetch("https://api.stackframe.com/health");
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
});
return {
cfTrace,
apiRoot,
baseUrlBackend,
prodDashboard,
prodBackend,
};
}
public async fetchNewAccessToken(refreshToken: RefreshToken) {
if (!('publishableClientKey' in this.options)) {
// TODO support it
@ -98,13 +147,29 @@ export class StackClientInterface {
});
return await Result.orThrowAsync(
Result.retry(
() => this.sendClientRequestInner(path, requestOptions, session!, requestType),
5,
{ exponentialDelayBase: 1000 },
)
const retriedResult = await Result.retry(
() => this.sendClientRequestInner(path, requestOptions, session!, requestType),
5,
{ exponentialDelayBase: 1000 },
);
// try to diagnose the error for the user
if (retriedResult.status === "error") {
if (!navigator.onLine) {
throw new Error("Failed to send Stack request. It seems like you are offline. (window.navigator.onLine is falsy)", { cause: retriedResult.error });
}
throw new Error(deindent`
Stack is unable to connect to the server. Please check your internet connection and try again.
If the problem persists, please contact Stack support and provide a screenshot of your entire browser console.
${retriedResult.error}
${JSON.stringify(await this.runNetworkDiagnostics(session, requestType), null, 2)}
`, { cause: retriedResult.error });
}
return retriedResult.data;
}
public createSession(options: Omit<ConstructorParameters<typeof InternalSession>[0], "refreshAccessTokenCallback">): InternalSession {

View File

@ -114,15 +114,20 @@ function mapResult<T, U, E = unknown, P = unknown>(result: AsyncResult<T, E, P>,
class RetryError extends AggregateError {
constructor(public readonly errors: unknown[]) {
const strings = errors.map(e => String(e));
const isAllSame = strings.length > 1 && strings.every(s => s === strings[0]);
super(
errors,
deindent`
Error after retrying ${errors.length} times.
${errors.map((e, i) => deindent`
Attempt ${i + 1}:
${e}
`).join("\n\n")}
${isAllSame ? deindent`
Attempts 1-${errors.length}:
${errors[0]}
` : errors.map((e, i) => deindent`
Attempt ${i + 1}:
${e}
`).join("\n\n")}
`,
{ cause: errors[errors.length - 1] }
);