Aperture Labs Insights

The “AJAX or Not?” Gotcha in nopCommerce (and a simple fix)

Written by Adam Ketterhagen | Dec 18, 2025

If you build on nopCommerce, here’s a small thing that can cause big head-scratching when requests hit your server:

WebHelper.IsAjaxRequest relies on the X-Requested-With: XMLHttpRequest header.

By default, many modern fetch calls don’t send it.

That means code paths that depend on “is this an AJAX request?” can behave inconsistently. Legacy jQuery .ajax() adds the header for you, but fetch() and many newer client libraries don’t. 

This results in a server that can’t reliably tell if the request is an asynchronous call intended for JSON/partial responses or if it’s a full-page request.

Below you’ll find details on why this matters, how to fix it, and a few options that balance compatibility, security, and maintainability.

 

Why you should care

Wrong response shape: If your server thinks the request is “not AJAX,” it might return a full HTML view when your client expects JSON. This cues cryptic errors.

Error handling: Many apps return partial views or JSON for AJAX requests and redirects for non-AJAX. Misclassification breaks user flows.

Caching & middleware: CDNs, proxies, and server middleware sometimes treat AJAX differently. Without the header, you can get odd cache behaviors.

Security checks: Some anti-CSRF or anti-automation rules are looser/tighter for AJAX calls. Mis-detection can trip false positives.

 

The root of the problem

X-Requested-With: XMLHttpRequest is a convention, not a browser requirement. Old-school XHR and jQuery .ajax() set it automatically, while fetch() does not. So in nopCommerce, WebHelper.IsAjaxRequest becomes a best-effort guess tied to a header the client might never send.

Server truth: By the time a request hits WebHelper.IsAjaxRequest, there’s no universal way to detect “AJAX” if the header isn’t present.

 

Pragmatic fixes (client side)

💡 Option A — Add the header in your fetch() calls

If you already control call sites:

await fetch('/some/url', {

  method: 'POST',

  headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json' },

  body: JSON.stringify({ fullName: 'Arnold Williamson' })

});

 

💡 Option B — A tiny wrapper (recommended)

Safer than monkey-patching globals, and easy to test and swap out.

export async function ajax(input, init = {}) {

  const headers = new Headers(init.headers || {});

  if (!headers.has('X-Requested-With')) {

    headers.set('X-Requested-With', 'XMLHttpRequest');

  }

  return fetch(input, { ...init, headers });

}

 

Usage:

const res = await ajax('/api/customer', { method: 'POST' });

 

💡 Option C — Global override (use with caution)

If you can’t touch every call site but must standardize now:

// keep reference

const _fetch = window.fetch;

 

window.fetch = function (input, init = {}) {

  const headers = new Headers(init?.headers || {});

  if (!headers.has('X-Requested-With')) {

    headers.set('X-Requested-With', 'XMLHttpRequest');

  }

  return _fetch(input, { ...init, headers });

};

 

Caveats: global overrides can complicate debugging, testing, and 3rd-party scripts. Prioritize a wrapper where possible.

 

jQuery .ajax() still works (and plays nice with async/await)

If your team prefers jQuery for legacy reasons (or because a client insists) you can still write modern, readable code:

const result = await $.ajax({

  url: '/some/url',

  method: 'POST',

  data: { fullName: 'Arnold Williamson' }

});

 

jQuery will include X-Requested-With for you. 

 

Server-side guardrails (nopCommerce / ASP.NET)

Even with client fixes, consider server resilience:

  1. Be explicit about response type
    When possible, route AJAX endpoints to controllers that always return JSON (or a partial) regardless of the header. Avoid logic like “if AJAX then JSON else View” where practical.
  2. Feature flags instead of header checks
    If you’re branching major behavior, consider a query param, Accept header, or URL pattern (/api/...) rather than “is AJAX?” heuristics.
  3. Middleware sanity checks
    Log requests missing X-Requested-With that hit AJAX endpoints. You’ll surface stray call sites quickly.

 

Security notes

  • CSRF: Don’t rely on X-Requested-With as a CSRF defense. Keep standard anti-forgery tokens in place.
  • CORS: Adding the header won’t auto-break CORS, but it becomes a non-simple request in some setups. Ensure your server’s CORS policy allows the header if you’re calling across origins.

Rollout plan (low risk)

  1. Add an ajax() wrapper and migrate hot paths first (checkout, cart, account)
  2. Log server calls to AJAX endpoints that lack the header for a week.
  3. Mop up the stragglers.
  4. Optionally, add a temporary global override during the transition for belt-and-suspenders coverage.

In summary

  • nopCommerce’s WebHelper.IsAjaxRequest depends on a header modern fetch() doesn’t send.
  • Add X-Requested-With: XMLHttpRequest via a small wrapper (preferred) or global override.
  • Where possible, stop branching solely on “is AJAX?”. Instead, favor explicit routes and response types.
  • jQuery .ajax() is still perfectly fine and works with async/await.

 

If you’d like a quick audit of your nopCommerce front end, Aperture Labs can scan your call sites, add a lightweight wrapper, and harden server endpoints so your UI behaves predictably (without a big refactor). Reach out here to get in touch!