diff --git a/packages/host/tests/helpers/index.gts b/packages/host/tests/helpers/index.gts index 23467637f5..90e6d01839 100644 --- a/packages/host/tests/helpers/index.gts +++ b/packages/host/tests/helpers/index.gts @@ -4,7 +4,7 @@ import { findAll, waitUntil, waitFor, click } from '@ember/test-helpers'; import { buildWaiter } from '@ember/test-waiters'; import GlimmerComponent from '@glimmer/component'; -import { formatRFC7231, parse } from 'date-fns'; +import { parse } from 'date-fns'; import ms from 'ms'; @@ -18,8 +18,6 @@ import { RealmInfo, RealmPermissions, Deferred, - executableExtensions, - SupportedMimeType, type TokenClaims, } from '@cardstack/runtime-common'; @@ -35,7 +33,6 @@ import { type EntrySetter, type SearchEntryWithErrors, } from '@cardstack/runtime-common/search-index'; -import { getFileWithFallbacks } from '@cardstack/runtime-common/stream'; import CardPrerender from '@cardstack/host/components/card-prerender'; @@ -557,10 +554,6 @@ async function setupTestRealm({ permissions, realmSecretSeed: testRealmSecretSeed, }); - loader.prependURLHandlers([ - (req) => sourceFetchRedirectHandle(req, adapter, realm), - (req) => sourceFetchReturnUrlHandle(req, realm.maybeHandle.bind(realm)), - ]); await realm.ready; return { realm, adapter }; @@ -890,87 +883,6 @@ export function diff( }; } -function isCardSourceFetch(request: Request) { - return ( - request.method === 'GET' && - request.headers.get('Accept') === SupportedMimeType.CardSource && - request.url.includes(testRealmURL) - ); -} - -export async function sourceFetchReturnUrlHandle( - request: Request, - defaultHandle: (req: Request) => Promise, -) { - if (isCardSourceFetch(request)) { - let r = await defaultHandle(request); - if (r) { - return new MockRedirectedResponse(r.body, r, request.url) as Response; - } - } - return null; -} - -export async function sourceFetchRedirectHandle( - request: Request, - adapter: RealmAdapter, - realm: Realm, -) { - let urlParts = new URL(request.url).pathname.split('.'); - if ( - isCardSourceFetch(request) && - urlParts.length === 1 //has no extension - ) { - const realmPaths = new RealmPaths(realm.url); - const localPath = realmPaths.local(request.url); - const ref = await getFileWithFallbacks( - localPath, - adapter.openFile.bind(adapter), - executableExtensions, - ); - let maybeExtension = ref?.path.split('.').pop(); - let responseUrl = maybeExtension - ? `${request.url}.${maybeExtension}` - : request.url; - - if ( - ref && - (ref.content instanceof ReadableStream || - ref.content instanceof Uint8Array || - typeof ref.content === 'string') - ) { - let r = createResponse(realm, ref.content, { - headers: { - 'last-modified': formatRFC7231(ref.lastModified), - }, - }); - return new MockRedirectedResponse(r.body, r, responseUrl) as Response; - } - } - return null; -} - -export class MockRedirectedResponse extends Response { - private _mockUrl: string; - - constructor( - body?: BodyInit | null | undefined, - init?: ResponseInit, - url?: string, - ) { - super(body, init); - this._mockUrl = url || ''; - } - - get redirected() { - return true; - } - - get url() { - return this._mockUrl; - } -} - export async function elementIsVisible(element: Element) { return new Promise((resolve) => { let intersectionObserver = new IntersectionObserver(function (entries) { diff --git a/packages/realm-server/tests/loader-test.ts b/packages/realm-server/tests/loader-test.ts index 88b5ae2aa3..304708274a 100644 --- a/packages/realm-server/tests/loader-test.ts +++ b/packages/realm-server/tests/loader-test.ts @@ -178,4 +178,28 @@ module('loader', function (hooks) { let testingLoader = Loader.getLoaderFor(card); assert.strictEqual(testingLoader, loader, 'the loaders are the same'); }); + + test('is able to follow redirects', async function (assert) { + loader.prependURLHandlers([ + async (request) => { + if (request.url.includes('node-b.abc')) { + return new Response('final redirection url'); + } + return null; + }, + async (request) => { + if (!request.url.includes('node-a.abc')) { + return null; + } + return new Response('redirected', { + status: 301, + headers: new Headers({ Location: `http://node-b.abc` }), + }); + }, + ]); + + let response = await loader.fetch(`http://node-a.abc`); + assert.strictEqual(response.url, 'http://node-b.abc/'); + assert.true(response.redirected); + }); }); diff --git a/packages/runtime-common/loader.ts b/packages/runtime-common/loader.ts index 406c1901d4..46f6a8031a 100644 --- a/packages/runtime-common/loader.ts +++ b/packages/runtime-common/loader.ts @@ -495,17 +495,53 @@ export class Loader { } } + // For following redirects of responses returned by loader's urlHandlers + private async simulateFetch( + request: Request, + result: Response, + ): Promise { + const urlString = request.url; + let redirectedHeaderKey = 'simulated-fetch-redirected'; // Temporary header to track if the request was redirected in the redirection chain + + if (result.status >= 300 && result.status < 400) { + const location = result.headers.get('location'); + if (location) { + request.headers.set(redirectedHeaderKey, 'true'); + return await this.fetch(new URL(location, urlString), request); + } + } + + // We are using Object.defineProperty because `url` and `redirected` + // response properties are read-only. We are overriding these properties to + // conform to the Fetch API specification where the `url` property is set to + // the final URL and the `redirected` property is set to true if the request + // was redirected. Normally, when using a native fetch, these properties are + // set automatically by the client, but in this case, we are simulating the + // fetch and need to set these properties manually. + + if (request.url && !result.url) { + Object.defineProperty(result, 'url', { value: urlString }); + + if (request.headers.get(redirectedHeaderKey) === 'true') { + Object.defineProperty(result, 'redirected', { value: true }); + request.headers.delete(redirectedHeaderKey); + } + } + + return result; + } + async fetch( urlOrRequest: string | URL | Request, init?: RequestInit, ): Promise { try { for (let handler of this.urlHandlers) { - let result = await handler( - this.asUnresolvedRequest(urlOrRequest, init), - ); + let request = this.asUnresolvedRequest(urlOrRequest, init); + + let result = await handler(request); if (result) { - return result; + return await this.simulateFetch(request, result); } } return await getNativeFetch()(this.asResolvedRequest(urlOrRequest, init));