dotnet/aspnetcore · error

Cannot perform enhanced form submission that changes the URL

Error message

Cannot perform enhanced form submission that changes the URL (except via a redirection), because then back/forward would not work. Either remove this form's 'action' attribute, or change its method to 'get', or do not mark it as enhanced.
Old URL: ${location.href}
New URL: ${response.url}

What it means

Thrown by performEnhancedPageLoad (NavigationEnhancement.ts:362) after SSR completes, when a non-GET enhanced form post succeeded and changed the URL path without issuing a redirection. Browser back/forward relies on GET idempotency ('Resubmit form?' prompt); a POST that lands on a new URL without redirect cannot be re-requested, so history would break. Blazor defers the throw until after SSR completes in case the server emits a late redirect signal.

Source

Thrown at src/Components/Web.JS/src/Services/NavigationEnhancement.ts:362

    // has since started). So finally, recreate the native "scroll to hash" behavior.
    const hashPosition = internalDestinationHref.indexOf('#');
    if (hashPosition >= 0) {
      const hash = internalDestinationHref.substring(hashPosition + 1);
      const targetElem = document.getElementById(hash);
      targetElem?.scrollIntoView();
    }

    performingEnhancedPageLoad = false;
    navigationEnhancementCallbacks.enhancedNavigationCompleted();

    // For non-GET requests, the destination has to be the same URL you're already on, or result in a redirection
    // (post/redirect/get). You're not allowed to POST to a different URL without redirecting, because then back/forwards
    // won't work - we can't recreate the "Resubmit form?" behavior.
    // See https://github.com/dotnet/aspnetcore/issues/50945
    // The reason we delay throwing until after SSR completes is that SSR might include a redirection signal. If we get
    // here without navigating away, it's an error.
    if (isNonRedirectedPostToADifferentUrlMessage) {
      throw new Error(isNonRedirectedPostToADifferentUrlMessage);
    }
  }
}

async function getResponsePartsWithFraming(responsePromise: Promise<Response>, abortSignal: AbortSignal, onInitialDocument: (response: Response, initialDocumentText: string) => void, onStreamingElement: (streamingElementMarkup) => void) {
  let response: Response;

  try {
    response = await responsePromise;

    if (!response.body) { // Not sure how this can happen, but the TypeScript annotations suggest it can
      onInitialDocument(response, '');
      return;
    }

    const frameBoundary = response.headers.get('ssr-framing');
    if (!frameBoundary) {
      // Shouldn't happen, but perhaps some proxy stripped the headers. In that case we just won't respect streaming and will

View on GitHub (pinned to 294cab2f9b)

Solutions

  1. Follow Post/Redirect/Get: have the POST handler return a 30x redirect to a GET endpoint.
  2. Remove the form's 'action' attribute so it posts to the current URL.
  3. Change the form method to 'get'.
  4. Remove data-enhance from the form.

Example fix

<!-- before -->
<form method="post" data-enhance action="/save">

<!-- after: PRG pattern -->
<form method="post" data-enhance action="/save">
<!-- server: return Redirect("/saved") after processing -->
Defensive patterns

Strategy: validation

Validate before calling

function followsPrgPattern(form: HTMLFormElement): boolean {
  const method = (form.getAttribute('method') || 'get').toLowerCase();
  if (method === 'get') return true;
  // POST forms should not change the URL path unless they redirect
  try {
    const u = new URL(form.getAttribute('action') || form.action, document.baseURI);
    return isForSamePath(u.href, location.href);
  } catch { return false; }
}

Type guard

function isUrlChangingPost(form: HTMLFormElement): boolean {
  const method = (form.getAttribute('method') || 'get').toLowerCase();
  if (method === 'get') return false;
  try {
    const u = new URL(form.getAttribute('action') || form.action, document.baseURI);
    return !isForSamePath(u.href, location.href);
  } catch { return true; }
}

Try / catch

try {
  await performEnhancedPageLoad(url, false, fetchOptions);
} catch (e) {
  if (/changes the URL/.test((e as Error).message)) {
    // instruct server to follow Post/Redirect/Get, or drop data-enhance
  } else { throw e; }
}

Prevention

When it happens

Trigger: An enhanced form (data-enhance) with method POST and an 'action' pointing to a different path than the current page, where the server returns 2xx without a 30x redirect and without the blazor-enhanced-nav-redirect-location header.

Common situations: Posting to /save and having the handler return content directly at /save (post/redirect/get pattern not followed); setting form action to a different URL than the page; mixing form posts with URL changes intentionally.

Related errors


AI-assisted analysis of dotnet/aspnetcore@294cab2f9b (2026-08-06). Data as JSON: /api/errors/c4cfeda28086916a. Report an issue: GitHub.