Skip to content

[Blazor] Fix JSInitializer URL computation for base URLs with query parameters #63185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/Components/Web.JS/src/JSInitializers/JSInitializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ export class JSInitializer {
await Promise.all(initializerFiles.map(f => importAndInvokeInitializer(this, f)));

function adjustPath(path: string): string {
// This is the same we do in JS interop with the import callback
const base = document.baseURI;
path = base.endsWith('/') ? `${base}${path}` : `${base}/${path}`;
return path;
// Use URL constructor to properly resolve relative paths, avoiding issues with query parameters
// This is the same mechanism as for import dotnet.js in MonoPlatform.ts
return new URL(path, document.baseURI).toString();
}

async function importAndInvokeInitializer(jsInitializer: JSInitializer, asset: JSAsset): Promise<void> {
Expand Down
98 changes: 98 additions & 0 deletions src/Components/Web.JS/test/JSInitializers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

describe('JSInitializer URL handling', () => {
// Mock document.baseURI for testing
let originalBaseURI: string;

beforeEach(() => {
originalBaseURI = document.baseURI;
});

afterEach(() => {
// Reset document.baseURI by setting a new base element
const existingBase = document.querySelector('base');
if (existingBase) {
existingBase.remove();
}
const base = document.createElement('base');
base.href = originalBaseURI;
document.head.insertBefore(base, document.head.firstChild);
});

function setDocumentBase(baseUri: string): void {
// Remove existing base elements
const existingBases = document.querySelectorAll('base');
existingBases.forEach(base => base.remove());

// Add new base element
const base = document.createElement('base');
base.href = baseUri;
document.head.insertBefore(base, document.head.firstChild);
}

// Test the correct URL resolution approach that the fix implements
function correctUrlResolution(path: string): string {
return new URL(path, document.baseURI).toString();
}

// Test the problematic string concatenation approach that was fixed
function problematicStringConcatenation(path: string): string {
const base = document.baseURI;
return base.endsWith('/') ? `${base}${path}` : `${base}/${path}`;
}

test('URL constructor vs string concatenation with query parameters', () => {
setDocumentBase('http://domain?a=x');

const correctResult = correctUrlResolution('_content/Package/file.js');
const problematicResult = problematicStringConcatenation('_content/Package/file.js');

// The URL constructor produces a valid URL
expect(correctResult).toBe('http://domain/_content/Package/file.js');

// String concatenation produces malformed URL with query in wrong place
expect(problematicResult).toBe('http://domain/?a=x/_content/Package/file.js');

// Verify they are different
expect(correctResult).not.toBe(problematicResult);
});

test('URL constructor handles hash correctly', () => {
setDocumentBase('http://domain#section');
const result = correctUrlResolution('_content/Package/file.js');

// Hash should be dropped when resolving relative URLs
expect(result).toBe('http://domain/_content/Package/file.js');
});

test('URL constructor handles trailing slash correctly', () => {
setDocumentBase('http://domain/');
const result = correctUrlResolution('_content/Package/file.js');

expect(result).toBe('http://domain/_content/Package/file.js');
});

test('URL constructor handles no trailing slash correctly', () => {
setDocumentBase('http://domain');
const result = correctUrlResolution('_content/Package/file.js');

expect(result).toBe('http://domain/_content/Package/file.js');
});

test('URL constructor resolves from subdirectory correctly', () => {
setDocumentBase('http://domain/subdir/');
const result = correctUrlResolution('_content/Package/file.js');

// From a subdirectory base, the relative path resolves relative to that directory
expect(result).toBe('http://domain/subdir/_content/Package/file.js');
});

test('URL constructor handles complex base with query and hash', () => {
setDocumentBase('http://domain/app/?debug=true#section');
const result = correctUrlResolution('_content/Package/file.js');

// Query and hash are handled properly - query is dropped, hash is dropped
expect(result).toBe('http://domain/app/_content/Package/file.js');
});
});
Loading