Skip to content

Commit

Permalink
Follow redirects in the loader
Browse files Browse the repository at this point in the history
  • Loading branch information
jurgenwerk committed Mar 18, 2024
1 parent 4eed5de commit c6d9a6e
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 89 deletions.
85 changes: 0 additions & 85 deletions packages/host/tests/helpers/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,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 };
Expand Down Expand Up @@ -890,87 +886,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<Response | null>,
) {
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) {
Expand Down
24 changes: 24 additions & 0 deletions packages/realm-server/tests/loader-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
41 changes: 37 additions & 4 deletions packages/runtime-common/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,17 +495,50 @@ export class Loader {
}
}

// For following redirects of responses returned by loader's urlHandlers
async simulateFetch(request: Request, result: Response): Promise<Response> {
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<Response> {
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));
Expand Down

0 comments on commit c6d9a6e

Please sign in to comment.