Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle location redirection URL error in getSharingInfo call in odsp driver #21277

Merged
merged 7 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions packages/drivers/odsp-driver/src/getFileLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import {
OdspErrorTypes,
OdspResourceTokenFetchOptions,
TokenFetcher,
type IOdspErrorAugmentations,
} from "@fluidframework/odsp-driver-definitions/internal";
import { ITelemetryLoggerExt, PerformanceEvent } from "@fluidframework/telemetry-utils/internal";
import {
ITelemetryLoggerExt,
PerformanceEvent,
isFluidError,
} from "@fluidframework/telemetry-utils/internal";

import { getUrlAndHeadersWithAuth } from "./getUrlAndHeadersWithAuth.js";
import {
Expand Down Expand Up @@ -55,7 +60,12 @@ export async function getFileLink(
fileLinkCore = await runWithRetry(
async () =>
runWithRetryForCoherencyAndServiceReadOnlyErrors(
async () => getFileLinkCore(getToken, odspUrlParts, logger),
async () =>
getFileLinkWithLocationRedirectionHandling(
getToken,
odspUrlParts,
logger,
),
"getFileLinkCore",
logger,
),
Expand Down Expand Up @@ -95,6 +105,48 @@ export async function getFileLink(
return fileLink;
}

/**
* Handles location redirection while fulfilling the getFilelink call. We don't want browser to handle
* the redirection as the browser will retry with same auth token which will not work as we need app
* to regenerate tokens for the new site domain.
* @param getToken - token fetcher to fetch the token.
* @param odspUrlParts - parts of odsp resolved url.
* @param logger - logger to send events.
* @returns Response from the API call.
* @alpha
*/
async function getFileLinkWithLocationRedirectionHandling(
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
odspUrlParts: IOdspUrlParts,
logger: ITelemetryLoggerExt,
): Promise<string> {
// We can have chains of location redirection one after the other, so have a for loop
// so that we can keep handling the same type of error.
for (;;) {
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
try {
return await getFileLinkCore(getToken, odspUrlParts, logger);
} catch (error: unknown) {
if (
isFluidError(error) &&
error.errorType === OdspErrorTypes.fileNotFoundOrAccessDeniedError
) {
const redirectLocation = (error as IOdspErrorAugmentations).redirectLocation;
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
if (redirectLocation !== undefined) {
logger.sendTelemetryEvent({
eventName: "LocationRedirectionErrorForGetOdspFileLink",
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
});
// Generate the new SiteUrl from the redirection location.
const newSiteDomain = new URL(redirectLocation).origin;
const newSiteUrl = `${newSiteDomain}${new URL(odspUrlParts.siteUrl).pathname}`;
odspUrlParts.siteUrl = newSiteUrl;
continue;
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
}
}
throw error;
}
}
}

async function getFileLinkCore(
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
odspUrlParts: IOdspUrlParts,
Expand Down Expand Up @@ -133,11 +185,16 @@ async function getFileLinkCore(
storageToken,
true,
);
// The location of file can move on Spo in which case server returns 308(Permanent Redirect) error.
// Adding prefer: manualredirect header will make VROOM API return 404 instead of 308 and browser can not intercept it.
// This error thrown by server will contain the new redirect location. Look at the 404 error parsing
// for further reference here: \packages\utils\odsp-doclib-utils\src\odspErrorUtils.ts
const requestInit = {
method: "POST",
headers: {
"Content-Type": "application/json;odata=verbose",
"Accept": "application/json;odata=verbose",
"prefer": "manualredirect",
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
...headers,
},
};
Expand Down Expand Up @@ -219,6 +276,11 @@ async function getFileItemLite(
storageToken,
forceAccessTokenViaAuthorizationHeader,
);
// The location of file can move on Spo in which case server returns 308(Permanent Redirect) error.
// Adding prefer: manualredirect header will make VROOM API return 404 instead of 308 and browser can not intercept it.
// This error thrown by server will contain the new redirect location. Look at the 404 error parsing
// for further reference here: \packages\utils\odsp-doclib-utils\src\odspErrorUtils.ts
headers.prefer = "manualredirect";
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
const requestInit = { method: "GET", headers };
const response = await fetchHelper(url, requestInit);
additionalProps = response.propsToLog;
Expand Down
98 changes: 98 additions & 0 deletions packages/drivers/odsp-driver/src/test/getFileLink.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {

describe("getFileLink", () => {
const siteUrl = "https://microsoft.sharepoint-df.com/siteUrl";
const newSiteUrl = "https://microsoft.sharepoint.com/siteUrl";
const driveId = "driveId";
const logger = new MockLogger();
const storageTokenFetcher = async (): Promise<string> => "StorageToken";
Expand Down Expand Up @@ -145,4 +146,101 @@ describe("getFileLink", () => {
"did not retries 5 times",
);
});

it("should handle location redirection once", async () => {
const result = await mockFetchMultiple(
async () =>
getFileLink(
storageTokenFetcher,
{ siteUrl, driveId, itemId: "itemId8" },
logger.toTelemetryLogger(),
),
[
async (): Promise<MockResponse> =>
createResponse(
{},
{
error: {
"message": "locationMoved",
"@error.redirectLocation": newSiteUrl,
},
},
404,
jatgarg marked this conversation as resolved.
Show resolved Hide resolved
),
async (): Promise<MockResponse> => okResponse({}, fileItemResponse),
async (): Promise<MockResponse> =>
okResponse({}, { d: { directUrl: "sharelink" } }),
],
);
assert.strictEqual(
result,
"sharelink",
"File link should match url returned from sharing information",
);
// Should be present in cache now and subsequent calls should fetch from cache.
const sharelink2 = await getFileLink(
storageTokenFetcher,
{ siteUrl, driveId, itemId: "itemId8" },
logger.toTelemetryLogger(),
);
assert.strictEqual(
sharelink2,
"sharelink",
"File link should match url returned from sharing information from cache",
);
});

it("should handle location redirection multiple times", async () => {
const result = await mockFetchMultiple(
async () =>
getFileLink(
storageTokenFetcher,
{ siteUrl, driveId, itemId: "itemId9" },
logger.toTelemetryLogger(),
),
[
async (): Promise<MockResponse> =>
createResponse(
{},
{
error: {
"message": "locationMoved",
"@error.redirectLocation": newSiteUrl,
},
},
404,
),
async (): Promise<MockResponse> =>
createResponse(
{},
{
error: {
"message": "locationMoved",
"@error.redirectLocation": newSiteUrl,
},
},
404,
),
async (): Promise<MockResponse> => okResponse({}, fileItemResponse),
async (): Promise<MockResponse> =>
okResponse({}, { d: { directUrl: "sharelink" } }),
],
);
assert.strictEqual(
result,
"sharelink",
"File link should match url returned from sharing information",
);
// Should be present in cache now and subsequent calls should fetch from cache.
const sharelink2 = await getFileLink(
storageTokenFetcher,
{ siteUrl, driveId, itemId: "itemId9" },
logger.toTelemetryLogger(),
);
assert.strictEqual(
sharelink2,
"sharelink",
"File link should match url returned from sharing information from cache",
);
});
});
Loading