From 1a692b983ac3af0ef7da4e0c38ea92dad81562b5 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Tue, 11 Mar 2025 09:39:51 +0000 Subject: [PATCH] Use fetch() in a way that works for file URLs (#3071) fetch returns a response code of 0 when it successfully loads a `file://` resource. This means we can't just rely on `response.ok`. Required for https://github.com/element-hq/element-call/issues/2994 --- src/config/Config.ts | 7 ++++--- src/initializer.tsx | 3 ++- src/soundUtils.ts | 4 +++- src/utils/fetch.test.ts | 30 ++++++++++++++++++++++++++++++ src/utils/fetch.ts | 25 +++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/utils/fetch.test.ts create mode 100644 src/utils/fetch.ts diff --git a/src/config/Config.ts b/src/config/Config.ts index d98bd9ec..079c4824 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -13,6 +13,7 @@ import { type ConfigOptions, type ResolvedConfigOptions, } from "./ConfigOptions"; +import { isFailure } from "../utils/fetch"; export class Config { private static internalInstance: Config | undefined; @@ -74,14 +75,14 @@ async function downloadConfig( configJsonFilename: string, ): Promise { const url = new URL(configJsonFilename, window.location.href); - const res = await fetch(url); + const response = await fetch(url); - if (!res.ok || res.status === 404 || res.status === 0) { + if (isFailure(response)) { // Lack of a config isn't an error, we should just use the defaults. // Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file: // URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI. return DEFAULT_CONFIG; } - return res.json(); + return response.json(); } diff --git a/src/initializer.tsx b/src/initializer.tsx index 614c14dc..0879490b 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -28,6 +28,7 @@ import { getUrlParams } from "./UrlParams"; import { Config } from "./config/Config"; import { ElementCallOpenTelemetry } from "./otel/otel"; import { platform } from "./Platform"; +import { isFailure } from "./utils/fetch"; // This generates a map of locale names to their URL (based on import.meta.url), which looks like this: // { @@ -79,7 +80,7 @@ const Backend = { }, }); - if (!response.ok) { + if (isFailure(response)) { throw Error(`Failed to fetch ${url}`); } diff --git a/src/soundUtils.ts b/src/soundUtils.ts index b8bcabbd..2c259c73 100644 --- a/src/soundUtils.ts +++ b/src/soundUtils.ts @@ -7,6 +7,8 @@ Please see LICENSE in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; +import { isFailure } from "./utils/fetch"; + type SoundDefinition = { mp3?: string; ogg: string }; export type PrefetchedSounds = Promise< @@ -49,7 +51,7 @@ export async function prefetchSounds( const response = await fetch( preferredFormat === "ogg" ? ogg : (mp3 ?? ogg), ); - if (!response.ok) { + if (isFailure(response)) { // If the sound doesn't load, it's not the end of the world. We won't play // the sound when requested, but it's better than failing the whole application. logger.warn(`Could not load sound ${name}, response was not okay`); diff --git a/src/utils/fetch.test.ts b/src/utils/fetch.test.ts new file mode 100644 index 00000000..16fddb7c --- /dev/null +++ b/src/utils/fetch.test.ts @@ -0,0 +1,30 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { expect, describe, it } from "vitest"; + +import { isFailure } from "./fetch"; + +describe("isFailure", () => { + it("returns false for a successful response", () => { + expect(isFailure({ ok: true, url: "https://foo.com" } as Response)).toBe( + false, + ); + }); + + it("returns true for a failed response", () => { + expect(isFailure({ ok: false, url: "https://foo.com" } as Response)).toBe( + true, + ); + }); + + it("returns false for a file:// URL with status 0", () => { + expect( + isFailure({ ok: false, url: "file://foo", status: 0 } as Response), + ).toBe(false); + }); +}); diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 00000000..ddc78ba5 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,25 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +/** + * Check if a fetch response is a failure in a way that works with file:// URLs + * @param response the response to check + * @returns true if the response is a failure, false otherwise + */ +export function isFailure(response: Response): boolean { + // if response says it's okay, then it's not a failure + if (response.ok) { + return false; + } + + // fetch will return status === 0 for a success on a file:// URL, so we special case it + if (response.url.startsWith("file:") && response.status === 0) { + return false; + } + + return true; +}