diff --git a/src/native/corehost/browserhost/loader/assets.ts b/src/native/corehost/browserhost/loader/assets.ts index 186869196b16fa..7edbcb151e9c20 100644 --- a/src/native/corehost/browserhost/loader/assets.ts +++ b/src/native/corehost/browserhost/loader/assets.ts @@ -7,7 +7,7 @@ import { dotnetAssert, dotnetLogger, dotnetInternals, dotnetBrowserHostExports, import { ENVIRONMENT_IS_WEB, ENVIRONMENT_IS_SHELL, ENVIRONMENT_IS_NODE } from "./per-module"; import { createPromiseCompletionSource, delay } from "./promise-completion-source"; import { locateFile, makeURLAbsoluteWithApplicationBase } from "./bootstrap"; -import { fetchLike } from "./polyfills"; +import { fetchLike, responseLike } from "./polyfills"; import { loaderConfig } from "./config"; let throttlingPCS: PromiseCompletionSource | undefined; @@ -39,7 +39,7 @@ export async function loadJSModule(asset: JsAsset): Promise { } assetInternal.behavior = "js-module-dotnet"; if (typeof loadBootResourceCallback === "function") { - const type = runtimeToBlazorAssetTypeMap[assetInternal.behavior]; + const type = behaviorToBlazorAssetTypeMap[assetInternal.behavior]; dotnetAssert.check(type, `Unsupported asset behavior: ${assetInternal.behavior}`); const customLoadResult = loadBootResourceCallback(type, assetInternal.name, asset.resolvedUrl!, assetInternal.integrity!, assetInternal.behavior); dotnetAssert.check(typeof customLoadResult === "string", "loadBootResourceCallback for JS modules must return string URL"); @@ -218,18 +218,16 @@ async function loadResourceRetry(asset: AssetEntryInternal): Promise { try { response = await loadResourceThrottle(asset); if (!response) { - response = { - ok: false, - status: -1, + response = responseLike(asset.resolvedUrl!, null, { + status: 404, statusText: "No response", - } as any; + }); } } catch (err: any) { - response = { - ok: false, - status: -1, + response = responseLike(asset.resolvedUrl!, null, { + status: 500, statusText: err.message || "Exception during fetch", - } as any; + }); } return response; } @@ -264,28 +262,24 @@ async function loadResourceThrottle(asset: AssetEntryInternal): Promise { + const expectedContentType = behaviorToContentTypeMap[asset.behavior]; + dotnetAssert.check(expectedContentType, `Unsupported asset behavior: ${asset.behavior}`); if (asset.buffer) { - return { - ok: true, + const arrayBuffer = await asset.buffer; + return responseLike(asset.resolvedUrl!, arrayBuffer, { + status: 200, + statusText: "OK", headers: { - length: 0, - get: () => null - }, - url: asset.resolvedUrl, - arrayBuffer: () => Promise.resolve(asset.buffer!), - json: () => { - throw new Error("NotImplementedException"); - }, - text: () => { - throw new Error("NotImplementedException"); + "Content-Length": arrayBuffer.byteLength.toString(), + "Content-Type": expectedContentType, } - }; + }); } if (asset.pendingDownload) { return asset.pendingDownload.response; } if (typeof loadBootResourceCallback === "function") { - const type = runtimeToBlazorAssetTypeMap[asset.behavior]; + const type = behaviorToBlazorAssetTypeMap[asset.behavior]; dotnetAssert.check(type, `Unsupported asset behavior: ${asset.behavior}`); const customLoadResult = loadBootResourceCallback(type, asset.name, asset.resolvedUrl!, asset.integrity!, asset.behavior); if (typeof customLoadResult === "string") { @@ -317,7 +311,7 @@ async function loadResourceFetch(asset: AssetEntryInternal): Promise { } } - return fetchLike(asset.resolvedUrl!, fetchOptions); + return fetchLike(asset.resolvedUrl!, fetchOptions, expectedContentType); } function onDownloadedAsset() { @@ -331,7 +325,7 @@ export function verifyAllAssetsDownloaded(): void { dotnetAssert.check(downloadedAssetsCount === totalAssetsToDownload, `Not all assets were downloaded. Downloaded ${downloadedAssetsCount} out of ${totalAssetsToDownload}`); } -const runtimeToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = { +const behaviorToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = { "resource": "assembly", "assembly": "assembly", "pdb": "pdb", @@ -344,3 +338,13 @@ const runtimeToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType "js-module-runtime": "dotnetjs", "js-module-threads": "dotnetjs" }; + +const behaviorToContentTypeMap: { [key: string]: string | undefined } = { + "resource": "application/octet-stream", + "assembly": "application/octet-stream", + "pdb": "application/octet-stream", + "icu": "application/octet-stream", + "vfs": "application/octet-stream", + "manifest": "application/json", + "dotnetwasm": "application/wasm", +}; diff --git a/src/native/corehost/browserhost/loader/polyfills.ts b/src/native/corehost/browserhost/loader/polyfills.ts index 923ff924612a5c..0b1cf55d303bd6 100644 --- a/src/native/corehost/browserhost/loader/polyfills.ts +++ b/src/native/corehost/browserhost/loader/polyfills.ts @@ -69,7 +69,7 @@ export async function nodeUrl(): Promise { return _nodeUrl; } -export async function fetchLike(url: string, init?: RequestInit): Promise { +export async function fetchLike(url: string, init?: RequestInit, expectedContentType?: string): Promise { try { await nodeFs(); await nodeUrl(); @@ -85,60 +85,66 @@ export async function fetchLike(url: string, init?: RequestInit): Promise{ - ok: true, + return responseLike(url, arrayBuffer, { + status: 200, + statusText: "OK", headers: { - length: 0, - get: () => null - }, - url, - arrayBuffer: () => arrayBuffer, - json: () => { - throw new Error("NotImplementedException"); - }, - text: () => { - throw new Error("NotImplementedException"); + "Content-Length": arrayBuffer.byteLength.toString(), + "Content-Type": expectedContentType || "application/octet-stream" } - }; + }); } else if (hasFetch) { return globalThis.fetch(url, init || { credentials: "same-origin" }); } else if (typeof (read) === "function") { - return { - ok: true, - url, + const arrayBuffer = read(url, "binary"); + return responseLike(url, arrayBuffer, { + status: 200, + statusText: "OK", headers: { - length: 0, - get: () => null - }, - arrayBuffer: () => { - return new Uint8Array(read(url, "binary")); - }, - json: () => { - return JSON.parse(read(url, "utf8")); - }, - text: () => read(url, "utf8") - }; + "Content-Length": arrayBuffer.byteLength.toString(), + "Content-Type": expectedContentType || "application/octet-stream" + } + }); } } catch (e: any) { - return { - ok: false, - url, + return responseLike(url, null, { status: 500, - headers: { - length: 0, - get: () => null - }, statusText: "ERR28: " + e, - arrayBuffer: () => { - throw e; - }, - json: () => { - throw e; - }, - text: () => { - throw e; - } - }; + headers: {}, + }); } throw new Error("No fetch implementation available"); } + +export function responseLike(url: string, body: ArrayBuffer | null, options: ResponseInit): Response { + if (typeof globalThis.Response === "function") { + const response = new Response(body, options); + + // Best-effort alignment with the fallback object shape: + // only define `url` if it does not already exist on the response. + if (typeof (response as any).url === "undefined") { + try { + Object.defineProperty(response, "url", { value: url }); + } catch { + // Ignore if the implementation does not allow redefining `url` + } + } + + return response; + } + return { + ok: body !== null && options.status === 200, + headers: { + ...options.headers, + get: (name: string) => (options.headers as any)[name] || null + }, + url, + arrayBuffer: () => Promise.resolve(body), + json: () => { + throw new Error("NotImplementedException"); + }, + text: () => { + throw new Error("NotImplementedException"); + } + }; +}