I'm working on a tracker that should collect some data on the websites of our clients and send it to our api using fetch request when site users leave the page.
The idea was to use beforeunload event handler to send the request, but I've read here that In order to cover most browsers I also need to use unload event handler.
This is the relevant part of tracker code that our clients will put on their websites:
var requestSent = false;
function submitData(element_id, url) {
if (!requestSent) {
var data = JSON.stringify({ourobject});
fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type':'application/x-www-form-urlencoded',
},
body: data,})
.then(response => response.json())
.then((data) => {
console.log('Hello?');
requestSent = true;
});
}
}
window.addEventListener('beforeunload', function (e) { submitData(1, "https://oursiteurl/metrics");});
window.addEventListener('unload', function(event) {submitData(1, "https://oursiteurl/metrics"); });
I've tested this on chrome and both requests pass, instead of just the first one that is successful, this leads to duplicate data in our database.
After putting console log in next to the part where requestSent flag is set to true, I realized that part of the code never gets executed.
If I preserve logs in network tab, it says that both requests are canceled, even though the data gets to our endpoint
Our api is created in Codeigniter, here is the /metrics endpoint
public function submit () {
$this->cors();
$response = [
'status' => 'error',
'message' => 'No data',
];
$data = json_decode(file_get_contents('php://input'), true);
if (empty($data)) {
echo json_encode($response);exit();
}
// process data and do other stuff ...
Cors function:
private function cors() {
// Allow from any origin
if (isset($_SERVER['HTTP_ORIGIN'])) {
// Decide if the origin in $_SERVER['HTTP_ORIGIN'] is one
// you want to allow, and if so:
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400'); // cache for 1 day
}
// Access-Control headers are received during OPTIONS requests
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
// may also be using PUT, PATCH, HEAD etc
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
}
EDIT:
Thanks to #CBroe for suggesting to use the Beacon API, using it removed the need for both unload and beforeunload event handlers:
submitData now looks like this:
...
if (navigator.sendBeacon) {
let beacon = navigator.sendBeacon(url, data);
console.log( 'Beacon', beacon );
} else { // fallback for older browsers
if (!requestSent) {
console.log( 'Data object from fallback', data );
var xhr = new XMLHttpRequest();
xhr.open("POST", url, false); // third parameter of `false` means synchronous
xhr.send(data);
}
...
Doing it this way allowed me to only keep beforeunload event handler because it works both on ie and chrome:
window.addEventListener('beforeunload', function (e) { submitData(1, "https://oursiteurl/metrics");});
The idea was to use beforeunload event handler to send the request, but I've read here that In order to cover most browsers I also need to use unload event handler.
Both are not terribly suited to make AJAX/fetch requests, they are likely to get cancelled when the page actually unloads.
You should rather use the Beacon API, that was specifically made for this kind of tracking / keep-alive requests.
According to the browser compability list there on MDN, it is not supported by Internet Explorer yet though. If you need tracking for that as well, maybe go with a two-pronged approach - Beacon for the browsers that support it, an AJAX/fetch fallback for IE.
Related
I am trying to set a session variable using fetch -
const response = await fetch('http://locahost/index.php/session/set', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({token:"token"})
});
The PHP function (inside a class) that does this -
public function setSession($arr){
try{
session_start();
$_SESSION['token'] = $arr['token'];
$responseData = json_encode("SESSION token has been set to ".$_SESSION['token']);
/// sendresponsedata() -> send response back with Access-Control-Allow-Credentials: true
} catch (Error $e) {
/// Some error
}
}
The PHP function is not on the same page as the page making the fetch request. When I console.log the response on the page that sent the request, it correctly shows SESSION token has been set to token.
But if then I try to retrieve the session variable using a different request and a different function -
fetch('http://localhost/index.php/session/get',{
credentials: 'include'
})
The response I get from this is always No ongoing session
public function getSession(){
try {
session_start();
// print json_encode($_SESSION); <---- printing this shows an empty array
$responseData = json_encode((isset($_SESSION["token"])) ? $_SESSION["token"]:"No ongoing session");
/// sendresponsedata() -> send response back with Access-Control-Allow-Credentials: true
} catch (Error $e) {
/// Some error
}
}
I looked at other questions like mine but as far as I could understand, the error was because of not allowing credentials. I couldn't really understand why credentials are needed in this case reading this, but I added them anyway to check first, but that didn't change anything. As far as I could understand fetch request creates a new session everytime so this could be impossible, but this might be possible if I made an AJAX request. I am not sure I understood that correctly however.
The sendresponsedata() function works well as I have made many other fetch requests with more headers, like allowing cross-origin requests and returning required headers on preflight handshakes which all worked (and it is not really a complicated function).
What am I doing wrong and how can I achieve what I need?
Edit: Since posting I have also tried xhr requests and they don't work either.
I am working on an internal web application at work. In IE10 the requests work fine, but in Chrome all the AJAX requests (which there are many) are sent using OPTIONS instead of whatever defined method I give it. Technically my requests are "cross domain." The site is served on localhost:6120 and the service I'm making AJAX requests to is on 57124. This closed jquery bug defines the issue, but not a real fix.
What can I do to use the proper http method in ajax requests?
Edit:
This is in the document load of every page:
jQuery.support.cors = true;
And every AJAX is built similarly:
var url = 'http://localhost:57124/My/Rest/Call';
$.ajax({
url: url,
dataType: "json",
data: json,
async: true,
cache: false,
timeout: 30000,
headers: { "x-li-format": "json", "X-UserName": userName },
success: function (data) {
// my success stuff
},
error: function (request, status, error) {
// my error stuff
},
type: "POST"
});
Chrome is preflighting the request to look for CORS headers. If the request is acceptable, it will then send the real request. If you're doing this cross-domain, you will simply have to deal with it or else find a way to make the request non-cross-domain. This is why the jQuery bug was closed as won't-fix. This is by design.
Unlike simple requests (discussed above), "preflighted" requests first
send an HTTP request by the OPTIONS method to the resource on the
other domain, in order to determine whether the actual request is safe
to send. Cross-site requests are preflighted like this since they may
have implications to user data. In particular, a request is
preflighted if:
It uses methods other than GET, HEAD or POST. Also, if POST is used to send request data with a Content-Type other than
application/x-www-form-urlencoded, multipart/form-data, or text/plain,
e.g. if the POST request sends an XML payload to the server using
application/xml or text/xml, then the request is preflighted.
It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)
Based on the fact that the request isn't sent on the default port 80/443 this Ajax call is automatically considered a cross-origin resource (CORS) request, which in other words means that the request automatically issues an OPTIONS request which checks for CORS headers on the server's/servlet's side.
This happens even if you set
crossOrigin: false;
or even if you ommit it.
The reason is simply that localhost != localhost:57124. Try sending it only to localhost without the port - it will fail, because the requested target won't be reachable, however notice that if the domain names are equal the request is sent without the OPTIONS request before POST.
I agree with Kevin B, the bug report says it all. It sounds like you are trying to make cross-domain ajax calls. If you're not familiar with the same origin policy you can start here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Same_origin_policy_for_JavaScript.
If this is not intended to be a cross-domain ajax call, try making your target url relative and see if the problem goes away. If you're really desperate look into the JSONP, but beware, mayhem lurks. There really isn't much more we can do to help you.
If it is possible pass the params through regular GET/POST with a different name and let your server side code handles it.
I had a similar issue with my own proxy to bypass CORS and I got the same error of POST->OPTION in Chrome. It was the Authorization header in my case ("x-li-format" and "X-UserName" here in your case.) I ended up passing it in a dummy format (e.g. AuthorizatinJack in GET) and I changed the code for my proxy to turn that into a header when making the call to the destination. Here it is in PHP:
if (isset($_GET['AuthorizationJack'])) {
$request_headers[] = "Authorization: Basic ".$_GET['AuthorizationJack'];
}
In my case I'm calling an API hosted by AWS (API Gateway). The error happened when I tried to call the API from a domain other than the API own domain. Since I'm the API owner I enabled CORS for the test environment, as described in the Amazon Documentation.
In production this error will not happen, since the request and the api will be in the same domain.
I hope it helps!
As answered by #Dark Falcon, I simply dealt with it.
In my case, I am using node.js server, and creating a session if it does not exist. Since the OPTIONS method does not have the session details in it, it ended up creating a new session for every POST method request.
So in my app routine to create-session-if-not-exist, I just added a check to see if method is OPTIONS, and if so, just skip session creating part:
app.use(function(req, res, next) {
if (req.method !== "OPTIONS") {
if (req.session && req.session.id) {
// Session exists
next();
}else{
// Create session
next();
}
} else {
// If request method is OPTIONS, just skip this part and move to the next method.
next();
}
}
"preflighted" requests first send an HTTP request by the OPTIONS method to the resource on the other domain, in order to determine whether the actual request is safe to send. Cross-site requests
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
Consider using axios
axios.get( url,
{ headers: {"Content-Type": "application/json"} } ).then( res => {
if(res.data.error) {
} else {
doAnything( res.data )
}
}).catch(function (error) {
doAnythingError(error)
});
I had this issue using fetch and axios worked perfectly.
I've encountered a very similar issue. I spent almost half a day to understand why everything works correctly in Firefox and fails in Chrome. In my case it was because of duplicated (or maybe mistyped) fields in my request header.
Use fetch instead of XHR,then the request will not be prelighted even it's cross-domained.
$.ajax({
url: '###',
contentType: 'text/plain; charset=utf-8',
async: false,
xhrFields: {
withCredentials: true,
crossDomain: true,
Authorization: "Bearer ...."
},
method: 'POST',
data: JSON.stringify( request ),
success: function (data) {
console.log(data);
}
});
the contentType: 'text/plain; charset=utf-8', or just contentType: 'text/plain', works for me!
regards!!
I am trying to access a public web service provided by USGS. According to the web page, they support CORS, and even provided a JQuery example (one thing worth to mention is that the example sets no header), but I tried everything and so far have no luck. There are lots of posts about cross-domain ajax and CORS on stackoverflow, but none has helped so far.
I tried both plain XMLHttpRequest and JQuery, with and without various headers, nothing worked. The plain one give back status code 0, which I believe it is an indicator that the request was blocked somewhere.
Anybody had successful experience with javascript CORS, either plain or with jquery?
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript">
function callWebService() {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
alert(xmlhttp.responseText);
} else {
alert(xmlhttp.status);
}
}
};
xmlhttp.open("GET", "http://waterservices.usgs.gov/nwis/iv/?format=json&sites=01646500¶meterCd=00060", true);
//xmlhttp.setRequestHeader("Accept","text/plain");
xmlhttp.setRequestHeader("Access-Control-Allow-Headers", "x-requested-with, x-requested-by, Content-Type");
xmlhttp.setRequestHeader("Access-Control-Allow-Origin", "*");
xmlhttp.setRequestHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
xmlhttp.setRequestHeader("Access-Control-Max-Age", "604800");
xmlhttp.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xmlhttp.send();
}
function callWebServiceWithJQuery() {
$(document).ajaxError(
function (event, jqXHR, ajaxSettings, thrownError) {
alert('[event:' + objToString(event) + '], [jqXHR:' + objToString(jqXHR) + '], [ajaxSettings:' + objToString(ajaxSettings) + '], [thrownError:' + objToString(thrownError) + '])');
});
$.ajax({
/*beforeSend: function (request) {
request.setRequestHeader("Access-Control-Allow-Headers", "x-requested-with, x-requested-by, Content-Type");
request.setRequestHeader("Access-Control-Allow-Origin", "*");
request.setRequestHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
request.setRequestHeader("Access-Control-Max-Age", "604800");
},*/
url: "http://waterservices.usgs.gov/nwis/iv/?format=json&sites=01646500¶meterCd=00060",
dataType:'json',
data:'',
success: function(response) {
alert("succ");
alert(response);
},
error: function(a,b,c) {
alert("err");
alert(a);
alert(b);
alert(c);
}
});
}
</script>
</head>
<body>
<form>
<button onclick="callWebService();">Click me</button>
</form>
</body>
</html>
You have two problems.
Leaving the page
You are triggering the Ajax request in response to a submit button being clicked.
Immediately after sending the request, the form submits and you leave the page, which causes the browser to abort the Ajax request.
The usual way to prevent the form from submitting when you are using an onclick attribute is to return false; from it. Now we are in the 21st century, however, I urge you to learn about addEventListener and move on from onclick.
Making a non-simple request
You are setting a bunch of custom request headers. These all require that the browser makes a pre-flight OPTIONS request to ask permission to make a cross-domain Ajax request with custom headers. The server doesn't grant permission for that. Don't set the custom request headers.
X-Requested-With is a non-standard (albeit common) hack to let a server send different content based on if the request is from Ajax or not (typically switching between JSON and an HTML document, something better suited to the Accept header). It isn't needed here. Don't set it.
Access-Control-Allow-etc are response headers. The server you are making the request to must respond with them to tell the browser that your site is allowed to use Ajax to access it. You can't set them on the client, it would be ridiculous for a site to grant itself permission to access a different site. Don't try to set these.
Following is the request i used so far
$http.get(url)
.success(function (data){})
.error(function (data){})
works without any CORS issues. My server side Allows all origins, methods, all headers
when i add http header like
$http.get(url, { headers: { "USERID": user, "SESSIONID": sessionId}})
the request changes into OPTIONS method when i see in chrome dev tools network tab
What is the reason for this? if it is expected then how to add custom http headers.
I have gone thru this link angularjs-performs-an-options-http-request-for-a-cross-origin-resource but it didnt help
Here i am expecting that server should allow different origins . But it is allowing headers, only if i were in a same server. But not sure about this is by angular or by server side.
after headers
$http.get(url,{ headers: { "USERID": user, "SESSIONID": sessionId } })
in chrome dev tools i am seeing like
Request Method:OPTIONS
Status Code:404 Not Found
but without headers
Request Method:GET
Status Code:200 OK
When i do this in REST Client, i can send headers to the backend.
$http({method: 'GET', url: '/someUrl', headers: { "USERID": user, "SESSIONID": sessionId}}).
success(function(data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
}).
error(function(data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
will work.
$http.get is a shortcut method.
Check the config in the docs
This is a known bug, see for instance https://github.com/angular/angular.js/issues/1585 .
A workaround is to use a jQuery request.
I had the same massive issue when trying to pass header in my get, where it changes get to options and wouldn't work. In order to make it work I added the following in my php api
<?php if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'GET') {
header("Access-Control-Allow-Headers: Authorization, X-Auth-Token");
}
exit;
} ?>
You can allow for any headers that you wish to pass.
Hope this helps
For my particular problem with my C# Web API solution I had to have something handle the Options request. Angular was sending a preflight request method OPTIONS which I did allow in my web.config with
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS, PATCH" />
But that wasn't enough I also included a method to handle the Options Request and I returned nothing
[ResponseType( typeof( void ) )]
public IHttpActionResult OptionsPost() {
return StatusCode( HttpStatusCode.NoContent );
}
I've got a pretty simple request set up to test CORS is working, as follows:
$.get( "http://www.otherdomain.com/thecontroller/test", function( data ) {
console.log(data);
});
I've even tried a vanilla javascript CORS request via the following..
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.otherdomain.com/thecontroller/test');
xhr.onreadystatechange = function () {
if (this.status == 200 && this.readyState == 4) {
console.log('response: ' + this.responseText);
}
};
xhr.send();
However these keeps getting the CORS error (No 'Access-Control-Allow-Origin' header is present on the requested resource) in both FF and Chrome.
On the server side I've tried quite a few things but for now just to test that I can get it working I'm allowing all CORS requests to the action, ie..
[EnableCors("*", "*", "*")]
public ActionResult Test()
{
//stuff
}
Am I missing something? Does Chrome / Firefox cache OPTIONS request? When I examine the network traffic in developer tools it only seems to be performing the GET method (and cancelling due to CORS) but it doesn't list any OPTIONS method.
I've even put a breakpoint in the Global.asax to try and catch the request, but it isn't even hitting it ie..
protected void Application_BeginRequest()
{
if (Request.HttpMethod == "OPTIONS")
{
//stuff
}
}
I'm at a bit of a loss now, any ideas?
A couple of thing you may try (caveat: I'm kinda green on this stuff, too)...
Did you enable CORS in your application Config class?
...
// Enable Cors
config.EnableCors();
...
Also, I think you're supposed to add the EnableCors[...] decorator on the controller, not the Action. At least that's how I've always done it.
namespace Your_API.Controllers
{
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class YourController : ApiController
{
For reference: This is a pretty solid walk-through I used in the past to get my own project going
As mentioned in a comment under my question, it turns out that I had to add a custom header to the jquery axax request. This forced the browser to send the OPTIONS request. This seems a bit hacky to me but it works, here is the solution:
$.ajax({
type: 'GET',
url: 'http://www.otherdomain.com/thecontroller/test',
headers: {
'MyCustomHeader': 'important information'
},
success: function() {
console.log('success');
},
error: function() {
console.log('failure');
}
});