This is an old question, but the existing answers are incorrect in 2024. It is now possible for server-side code to distinguishing incoming XHR requests from non-XHR requests by looking at the "Sec-Fetch-Dest" request header. In all modern browsers now the Sec-Fetch-Dest value for XHR requests is the literal string "empty". For non-XHR requests it's something else.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest