mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Better RetryErrors
This commit is contained in:
parent
1128e59b6c
commit
3bebd3f4d1
14
apps/backend/src/app/health/route.tsx
Normal file
14
apps/backend/src/app/health/route.tsx
Normal 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": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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
|
||||
|
||||
14
apps/dashboard/src/app/health/route.tsx
Normal file
14
apps/dashboard/src/app/health/route.tsx
Normal 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": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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] }
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user