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
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
type ConfigOptions,
|
type ConfigOptions,
|
||||||
type ResolvedConfigOptions,
|
type ResolvedConfigOptions,
|
||||||
} from "./ConfigOptions";
|
} from "./ConfigOptions";
|
||||||
|
import { isFailure } from "../utils/fetch";
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
private static internalInstance: Config | undefined;
|
private static internalInstance: Config | undefined;
|
||||||
@@ -74,14 +75,14 @@ async function downloadConfig(
|
|||||||
configJsonFilename: string,
|
configJsonFilename: string,
|
||||||
): Promise<ConfigOptions> {
|
): Promise<ConfigOptions> {
|
||||||
const url = new URL(configJsonFilename, window.location.href);
|
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.
|
// 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:
|
// 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.
|
// 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 DEFAULT_CONFIG;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { getUrlParams } from "./UrlParams";
|
|||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
import { ElementCallOpenTelemetry } from "./otel/otel";
|
import { ElementCallOpenTelemetry } from "./otel/otel";
|
||||||
import { platform } from "./Platform";
|
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:
|
// 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}`);
|
throw Error(`Failed to fetch ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { isFailure } from "./utils/fetch";
|
||||||
|
|
||||||
type SoundDefinition = { mp3?: string; ogg: string };
|
type SoundDefinition = { mp3?: string; ogg: string };
|
||||||
|
|
||||||
export type PrefetchedSounds<S extends string> = Promise<
|
export type PrefetchedSounds<S extends string> = Promise<
|
||||||
@@ -49,7 +51,7 @@ export async function prefetchSounds<S extends string>(
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
preferredFormat === "ogg" ? ogg : (mp3 ?? ogg),
|
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
|
// 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.
|
// the sound when requested, but it's better than failing the whole application.
|
||||||
logger.warn(`Could not load sound ${name}, response was not okay`);
|
logger.warn(`Could not load sound ${name}, response was not okay`);
|
||||||
|
|||||||
30
src/utils/fetch.test.ts
Normal file
30
src/utils/fetch.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
src/utils/fetch.ts
Normal file
25
src/utils/fetch.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user