mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-22 02:23:30 +00:00
Correctly handle errors from getPublishedContentByUrl (#142)
* Correctly handle errors from getPublishedContentByUrl * Format * Improve error message
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.33.0",
|
||||
"@gitbook/api": "^0.34.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
|
||||
+59
-33
@@ -56,10 +56,17 @@ export function withAPI<T>(client: GitBookAPI, fn: () => Promise<T>): Promise<T>
|
||||
return apiSyncStorage.run(client, fn);
|
||||
}
|
||||
|
||||
export type PublishedContentWithCache = PublishedContentLookup & {
|
||||
cacheMaxAge?: number;
|
||||
cacheTags?: string[];
|
||||
};
|
||||
export type PublishedContentWithCache =
|
||||
| (PublishedContentLookup & {
|
||||
cacheMaxAge?: number;
|
||||
cacheTags?: string[];
|
||||
})
|
||||
| {
|
||||
error: {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user by its ID.
|
||||
@@ -102,38 +109,57 @@ export const getPublishedContentByUrl = cache(
|
||||
// We call it as this logic is wrapped in an asynchronous cache that is not tied to the signal.
|
||||
signal?.throwIfAborted();
|
||||
|
||||
const response = await api().request<PublishedContentLookup>({
|
||||
method: 'GET',
|
||||
path: '/urls/published',
|
||||
query: {
|
||||
url,
|
||||
visitorAuthToken,
|
||||
},
|
||||
secure: false,
|
||||
format: 'json',
|
||||
signal: signal,
|
||||
...noCacheFetchOptions,
|
||||
});
|
||||
try {
|
||||
const response = await api().request<PublishedContentLookup>({
|
||||
method: 'GET',
|
||||
path: '/urls/published',
|
||||
query: {
|
||||
url,
|
||||
visitorAuthToken,
|
||||
},
|
||||
secure: false,
|
||||
format: 'json',
|
||||
signal: signal,
|
||||
...noCacheFetchOptions,
|
||||
});
|
||||
|
||||
const parsed = parseCacheResponse(response);
|
||||
const parsed = parseCacheResponse(response);
|
||||
|
||||
const tags = [
|
||||
...parsed.tags,
|
||||
...('space' in response.data
|
||||
? [getAPICacheTag({ tag: 'space', space: response.data.space })]
|
||||
: []),
|
||||
];
|
||||
const tags = [
|
||||
...parsed.tags,
|
||||
...('space' in response.data
|
||||
? [getAPICacheTag({ tag: 'space', space: response.data.space })]
|
||||
: []),
|
||||
];
|
||||
|
||||
const data: PublishedContentWithCache = {
|
||||
...response.data,
|
||||
cacheMaxAge: parsed.ttl,
|
||||
cacheTags: tags,
|
||||
};
|
||||
return {
|
||||
tags,
|
||||
ttl: parsed.ttl,
|
||||
data,
|
||||
};
|
||||
const data: PublishedContentWithCache = {
|
||||
...response.data,
|
||||
cacheMaxAge: parsed.ttl,
|
||||
cacheTags: tags,
|
||||
};
|
||||
return {
|
||||
tags,
|
||||
ttl: parsed.ttl,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
const httpError = error as GitBookAPIError;
|
||||
if (httpError.code < 500) {
|
||||
return {
|
||||
data: {
|
||||
error: {
|
||||
code: httpError.code,
|
||||
message: httpError.errorMessage || httpError.message,
|
||||
},
|
||||
} as PublishedContentWithCache,
|
||||
// Cache errors for max 10 minutes in case the user is making changes to its content configuration
|
||||
ttl: 60 * 10,
|
||||
tags: [],
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Do not pass the options for the cache key
|
||||
|
||||
+63
-23
@@ -99,9 +99,9 @@ export async function middleware(request: NextRequest) {
|
||||
}),
|
||||
() => lookupSpaceForURL(mode, inputURL, visitorAuthToken),
|
||||
);
|
||||
if (!resolved) {
|
||||
return new NextResponse(`Content not found`, {
|
||||
status: 404,
|
||||
if ('error' in resolved) {
|
||||
return new NextResponse(resolved.error.message, {
|
||||
status: resolved.error.code,
|
||||
headers: {
|
||||
'x-gitbook-version': buildVersion(),
|
||||
},
|
||||
@@ -237,7 +237,7 @@ async function lookupSpaceForURL(
|
||||
mode: URLLookupMode,
|
||||
url: URL,
|
||||
visitorAuthToken: string | undefined,
|
||||
): Promise<LookupResult | null> {
|
||||
): Promise<LookupResult> {
|
||||
switch (mode) {
|
||||
case 'single': {
|
||||
return await lookupSpaceInSingleMode(url);
|
||||
@@ -260,7 +260,7 @@ async function lookupSpaceForURL(
|
||||
* GITBOOK_MODE=single
|
||||
* When serving a single space, configured using GITBOOK_SPACE_ID and GITBOOK_TOKEN.
|
||||
*/
|
||||
async function lookupSpaceInSingleMode(url: URL): Promise<LookupResult | null> {
|
||||
async function lookupSpaceInSingleMode(url: URL): Promise<LookupResult> {
|
||||
const spaceId = process.env.GITBOOK_SPACE_ID;
|
||||
if (!spaceId) {
|
||||
throw new Error(
|
||||
@@ -290,7 +290,7 @@ async function lookupSpaceInSingleMode(url: URL): Promise<LookupResult | null> {
|
||||
async function lookupSpaceInMultiMode(
|
||||
url: URL,
|
||||
visitorAuthToken: string | undefined,
|
||||
): Promise<LookupResult | null> {
|
||||
): Promise<LookupResult> {
|
||||
return lookupSpaceByAPI(url, visitorAuthToken);
|
||||
}
|
||||
|
||||
@@ -298,24 +298,36 @@ async function lookupSpaceInMultiMode(
|
||||
* GITBOOK_MODE=multi-id
|
||||
* When serving multi spaces with the ID passed in the path.
|
||||
*/
|
||||
async function lookupSpaceInMultiIdMode(url: URL): Promise<LookupResult | null> {
|
||||
async function lookupSpaceInMultiIdMode(url: URL): Promise<LookupResult> {
|
||||
// Extract the iD from the path
|
||||
const pathSegments = url.pathname.slice(1).split('/');
|
||||
if (pathSegments[0] !== '~space') {
|
||||
throw new Error(`Invalid path, expected ~space`);
|
||||
return null;
|
||||
return {
|
||||
error: {
|
||||
code: 400,
|
||||
message: `Missing space ID in the path`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const spaceId = pathSegments[1];
|
||||
if (!spaceId) {
|
||||
throw new Error(`Missing space ID in the path`);
|
||||
return null;
|
||||
return {
|
||||
error: {
|
||||
code: 400,
|
||||
message: `Missing space ID in the path`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get the auth token from the URL query
|
||||
const apiToken = url.searchParams.get('token');
|
||||
if (!apiToken) {
|
||||
throw new Error(`Missing token query parameter`);
|
||||
return null;
|
||||
return {
|
||||
error: {
|
||||
code: 400,
|
||||
message: `Missing token query parameter`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const apiEndpoint = url.searchParams.get('api') ?? api().endpoint;
|
||||
@@ -346,28 +358,45 @@ async function lookupSpaceInMultiIdMode(url: URL): Promise<LookupResult | null>
|
||||
async function lookupSpaceInMultiPathMode(
|
||||
url: URL,
|
||||
visitorAuthToken: string | undefined,
|
||||
): Promise<LookupResult | null> {
|
||||
): Promise<LookupResult> {
|
||||
// Skip useless requests
|
||||
if (
|
||||
url.pathname === '/favicon.ico' ||
|
||||
url.pathname === '/robots.txt' ||
|
||||
url.pathname === '/sitemap.xml' ||
|
||||
// Match something that starts with a domain like
|
||||
!url.pathname.match(/^.+\..+/)
|
||||
url.pathname === '/sitemap.xml'
|
||||
) {
|
||||
return null;
|
||||
return {
|
||||
error: {
|
||||
code: 404,
|
||||
message: `favicon.ico, robots.txt, sitemap.xml should be accessed under a content`,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Only match something that starts with a domain like
|
||||
if (!url.pathname.match(/^.+\..+/)) {
|
||||
return {
|
||||
error: {
|
||||
code: 404,
|
||||
message: `Invalid URL in the path, should start with a domain`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const targetStr = `https://${url.pathname}`;
|
||||
|
||||
if (!URL.canParse(targetStr)) {
|
||||
throw new Error(`Invalid URL in the path`);
|
||||
return {
|
||||
error: {
|
||||
code: 404,
|
||||
message: `Invalid URL in the path`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const target = new URL(targetStr);
|
||||
|
||||
const lookup = await lookupSpaceByAPI(target, visitorAuthToken);
|
||||
if (!lookup) {
|
||||
return null;
|
||||
if ('error' in lookup) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
if ('redirect' in lookup) {
|
||||
@@ -400,7 +429,7 @@ async function lookupSpaceInMultiPathMode(
|
||||
async function lookupSpaceByAPI(
|
||||
url: URL,
|
||||
visitorAuthToken: string | undefined,
|
||||
): Promise<LookupResult | null> {
|
||||
): Promise<LookupResult> {
|
||||
const lookupAlternatives = getURLLookupAlternatives(stripURLSearch(url));
|
||||
|
||||
console.log(
|
||||
@@ -418,6 +447,10 @@ async function lookupSpaceByAPI(
|
||||
signal: abort.signal,
|
||||
});
|
||||
|
||||
if ('error' in data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if ('redirect' in data) {
|
||||
if (alternative.url === url.toString()) {
|
||||
return data;
|
||||
@@ -449,7 +482,14 @@ async function lookupSpaceByAPI(
|
||||
);
|
||||
|
||||
console.log(`lookup took ${Date.now() - startTime}ms`);
|
||||
return matches.find((match) => match !== null) ?? null;
|
||||
return (
|
||||
matches.find((match) => match !== null) ?? {
|
||||
error: {
|
||||
code: 404,
|
||||
message: `No content found`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function joinPath(...parts: string[]): string {
|
||||
|
||||
Reference in New Issue
Block a user