Skip to content

Commit

Permalink
[main > release/client/2.0]: Handle Location redirection for getShari…
Browse files Browse the repository at this point in the history
…ngInformation call (#22551) (#22622)

## Description


[AB#15188](https://dev.azure.com/fluidframework/internal/_workitems/edit/15188)

Handle location redirection when calling getSharingInformation in ODSP
Driver. Since this call does not include the v2.0 which calls the API
without the drive and item ID, these calls fail if the tenant has been
renamed. Sometimes this creates issues when creating a new page, since
the user could not get the sharing link to the new page. GetFileItem
call also returns the new siteUrl to which the file has been moved. Use
that to detect the new domain and then change the resolved url
accordingly.

---------

Co-authored-by: Jatin Garg <[email protected]>
Co-authored-by: Frank Mueller <[email protected]>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 18ef26c commit d2fecdf
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 94 deletions.
84 changes: 62 additions & 22 deletions packages/drivers/odsp-driver/src/getFileLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import type { ITelemetryBaseProperties } from "@fluidframework/core-interfaces";
import { assert } from "@fluidframework/core-utils/internal";
import { NonRetryableError, runWithRetry } from "@fluidframework/driver-utils/internal";
import { hasRedirectionLocation } from "@fluidframework/odsp-doclib-utils/internal";
import {
IOdspUrlParts,
OdspErrorTypes,
OdspResourceTokenFetchOptions,
TokenFetcher,
type IOdspResolvedUrl,
} from "@fluidframework/odsp-driver-definitions/internal";
import {
ITelemetryLoggerExt,
Expand Down Expand Up @@ -44,10 +44,10 @@ const fileLinkCache = new Map<string, Promise<string>>();
*/
export async function getFileLink(
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
odspUrlParts: IOdspUrlParts,
resolvedUrl: IOdspResolvedUrl,
logger: ITelemetryLoggerExt,
): Promise<string> {
const cacheKey = `${odspUrlParts.siteUrl}_${odspUrlParts.driveId}_${odspUrlParts.itemId}`;
const cacheKey = `${resolvedUrl.siteUrl}_${resolvedUrl.driveId}_${resolvedUrl.itemId}`;
const maybeFileLinkCacheEntry = fileLinkCache.get(cacheKey);
if (maybeFileLinkCacheEntry !== undefined) {
return maybeFileLinkCacheEntry;
Expand All @@ -61,7 +61,7 @@ export async function getFileLink(
async () =>
runWithRetryForCoherencyAndServiceReadOnlyErrors(
async () =>
getFileLinkWithLocationRedirectionHandling(getToken, odspUrlParts, logger),
getFileLinkWithLocationRedirectionHandling(getToken, resolvedUrl, logger),
"getFileLinkCore",
logger,
),
Expand Down Expand Up @@ -115,32 +115,41 @@ export async function getFileLink(
*/
async function getFileLinkWithLocationRedirectionHandling(
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
odspUrlParts: IOdspUrlParts,
resolvedUrl: IOdspResolvedUrl,
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. Set max number of redirection to 5.
let lastError: unknown;
let locationRedirected = false;
for (let count = 1; count <= 5; count++) {
try {
return await getFileLinkCore(getToken, odspUrlParts, logger);
const fileItem = await getFileItemLite(getToken, resolvedUrl, logger, true);
// Sometimes the siteUrl in the actual file is different from the siteUrl in the resolvedUrl due to location
// redirection. This creates issues in the getSharingInformation call. So we need to update the siteUrl in the
// resolvedUrl to the siteUrl in the fileItem which is the updated siteUrl.
const oldSiteDomain = new URL(resolvedUrl.siteUrl).origin;
const newSiteDomain = new URL(fileItem.sharepointIds.siteUrl).origin;
if (oldSiteDomain !== newSiteDomain) {
locationRedirected = true;
logger.sendTelemetryEvent({
eventName: "LocationRedirectionErrorForGetOdspFileLink",
retryCount: count,
});
renameTenantInOdspResolvedUrl(resolvedUrl, newSiteDomain);
}
return await getFileLinkCore(getToken, resolvedUrl, logger, fileItem);
} catch (error: unknown) {
lastError = error;
// If the getSharingLink call fails with the 401/403/404 error, then it could be due to that the file has moved
// to another location. This could occur in case we have more than 1 tenant rename. In that case, we should retry
// the getFileItemLite call to get the updated fileItem.
if (
isFluidError(error) &&
error.errorType === OdspErrorTypes.fileNotFoundOrAccessDeniedError &&
hasRedirectionLocation(error) &&
error.redirectLocation !== undefined
locationRedirected &&
(error.errorType === OdspErrorTypes.fileNotFoundOrAccessDeniedError ||
error.errorType === OdspErrorTypes.authorizationError)
) {
const redirectLocation = error.redirectLocation;
logger.sendTelemetryEvent({
eventName: "LocationRedirectionErrorForGetOdspFileLink",
retryCount: count,
});
// 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;
}
throw error;
Expand All @@ -153,9 +162,8 @@ async function getFileLinkCore(
getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
odspUrlParts: IOdspUrlParts,
logger: ITelemetryLoggerExt,
fileItem: FileItemLite,
): Promise<string> {
const fileItem = await getFileItemLite(getToken, odspUrlParts, logger, true);

// ODSP link requires extra call to return link that is resistant to file being renamed or moved to different folder
return PerformanceEvent.timedExecAsync(
logger,
Expand Down Expand Up @@ -193,7 +201,6 @@ async function getFileLinkCore(
headers: {
"Content-Type": "application/json;odata=verbose",
"Accept": "application/json;odata=verbose",
"redirect": "manual",
...headers,
},
};
Expand Down Expand Up @@ -280,7 +287,6 @@ async function getFileItemLite(
);

const headers = getHeadersWithAuth(authHeader);
headers.redirect = "manual";
const requestInit = { method, headers };
const response = await fetchHelper(url, requestInit);
additionalProps = response.propsToLog;
Expand All @@ -301,3 +307,37 @@ async function getFileItemLite(
},
);
}

/**
* It takes a resolved url with old siteUrl and patches resolved url with updated site url domain.
* @param odspResolvedUrl - Previous odsp resolved url with older site url.
* @param newSiteDomain - New site domain after the tenant rename.
*/
function renameTenantInOdspResolvedUrl(
odspResolvedUrl: IOdspResolvedUrl,
newSiteDomain: string,
): void {
const newSiteUrl = `${newSiteDomain}${new URL(odspResolvedUrl.siteUrl).pathname}`;
odspResolvedUrl.siteUrl = newSiteUrl;

if (odspResolvedUrl.endpoints.attachmentGETStorageUrl) {
odspResolvedUrl.endpoints.attachmentGETStorageUrl = `${newSiteDomain}${
new URL(odspResolvedUrl.endpoints.attachmentGETStorageUrl).pathname
}`;
}
if (odspResolvedUrl.endpoints.attachmentPOSTStorageUrl) {
odspResolvedUrl.endpoints.attachmentPOSTStorageUrl = `${newSiteDomain}${
new URL(odspResolvedUrl.endpoints.attachmentPOSTStorageUrl).pathname
}`;
}
if (odspResolvedUrl.endpoints.deltaStorageUrl) {
odspResolvedUrl.endpoints.deltaStorageUrl = `${newSiteDomain}${
new URL(odspResolvedUrl.endpoints.deltaStorageUrl).pathname
}`;
}
if (odspResolvedUrl.endpoints.snapshotStorageUrl) {
odspResolvedUrl.endpoints.snapshotStorageUrl = `${newSiteDomain}${
new URL(odspResolvedUrl.endpoints.snapshotStorageUrl).pathname
}`;
}
}
Loading

0 comments on commit d2fecdf

Please sign in to comment.