I've been trying to use service workers for (what seems like hours & hours), to attach a simple header to all requests. Whats frustrating is, it sort of works.
Attempt 1:
self.addEventListener("fetch", event => {
const modifiedHeaders = new Headers({
...event.request.headers,
'API-Key': '000000000000000000001'
});
const modifiedRequest = new Request(event.request, {
headers: modifiedHeaders,
});
event.respondWith((async () => {
return fetch(modifiedRequest);
})());
});
The above code works for HTML files however for CSS & JS files I get the follow error
ReferenceError: headers is not defined
If I disable the header requirement the page loads with images and javascript and I can interact with it like normal.
Attempt 2:
var req = new Request(event.request.url, {
headers: {
...event.request.headers,
'API-Key': '000000000000000000001'
},
method: event.request.method,
mode: event.request.mode,
credentials: event.request.credentials,
redirect: event.request.redirect,
referrer: event.request.referrer,
referrerPolicy: event.request.referrerPolicy,
bodyUsed: event.request.bodyUsed,
cache: event.request.cache,
destination: event.request.destination,
integrity: event.request.integrity,
isHistoryNavigation: event.request.isHistoryNavigation,
keepalive: event.request.keepalive
});
This attempt, I simply built a new request, which successfully included the new header on CSS & JS file requests. However, When I do a POST or redirect, things stop working and behave strange.
What is the correct approach for this? I feel that attempt 1 is the better path, but I can't seem to create the Headers object on the request no matter what I do.
The version of chrome I am using is
Version 78.0.3904.70 (Official Build) (64-bit)
The site is an internal developer tool so cross browser compatibility isn't required. So I'm happy to load any additional libs / enable experimental features etc.
The problem is that your modified requests reuse the mode of the original request in both of your attempts
For embedded resources where the request is initiated from markup (unless the crossorigin attribute is present) the request is in most cases made using the no-cors mode which only allows a very limited specific set of simple headers.
no-cors — Prevents the method from being anything other than HEAD, GET
or POST, and the headers from being anything other than simple
headers. If any ServiceWorkers intercept these requests, they may not
add or override any headers except for those that are simple headers...
Source and more info on request modes: https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
Simple headers are the following ones: accept (only some values), accept-language, content-language (only some values), content-type.
Source: https://fetch.spec.whatwg.org/#simple-header:
Solution:
You need to make sure to set the mode to something other than no-cors when creating the modified request. You can pick either cors or same-origin, depending on whether you want to allow cross-origin requests. (The navigate mode is reserved for navigation only and it is not possible to create a request with that mode.)
Why your code worked for HTML files:
The request issued when navigating to a new page uses the navigate mode. Chrome does not allow creating requests with this mode using the new Request() constructor, but seems to automatically silently use the same-origin mode when an existing request with the navigate mode is passed to the constructor as a parameter. This means that your first (HTML load) modified request had same-origin mode, while the CSS and JS load requests had the no-cors mode.
Working example:
'use strict';
/* Auxiliary function to log info about requests to the console */
function logRequest(message, req) {
const headersString = [...req.headers].reduce((outputString, val) => `${outputString}\n${val[0]}: ${val[1]}`, 'Headers:');
console.log(`${message}\nRequest: ${req.url}\nMode: ${req.mode}\n${headersString}\n\n`);
}
self.addEventListener('fetch', (event) => {
logRequest('Fetch event detected', event.request);
const modifiedHeaders = new Headers(event.request.headers);
modifiedHeaders.append('API-Key', '000000000000000000001');
const modifiedRequestInit = { headers: modifiedHeaders, mode: 'same-origin' };
const modifiedRequest = new Request(event.request, modifiedRequestInit);
logRequest('Modified request', modifiedRequest);
event.respondWith((async () => fetch(modifiedRequest))());
});
I would try this:
self.addEventListener("fetch", event => {
const modifiedRequest = new Request(event.request, {
headers: {
'API-Key': '000000000000000000001'
},
});
event.respondWith((async () => {
return fetch(modifiedRequest);
})());
});
Related
I want to know if it is possible to open a browser with header authentication by Javascript?
I can assign header to the request in Postman when calling API.
But I can't find if it is reasonable to expect browser can do the same thing?
If yes, any example to trigger the browser with authentication header?
Thanks
Assuming you can get the browser to load a page that runs JavaScript then you can execute follow-on API calls and attach headers to them. There are a variety of frameworks available or you can use the ES6 Fetch API that runs fine in most modern browsers.
Ref Fetch Docs:
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
Ref Headers specifically for Fetch:
https://developer.mozilla.org/en-US/docs/Web/API/Headers
Users could use a local file on their machine which has the script to fetch the data... or you could have users hit an endpoint that requires no authentication and merely serves to host up the page that has the API calls, gathers their credentials and then requests the authenticated data from the protected endpoints.
const myHeaders = new Headers();
myHeaders.set('Authentication', 'basic <insert base64 encoded basic auth string>');
const url = 'https://www.example.com/api/content';
const requestObj = {
method: 'GET',
headers: myHeaders,
mode: 'cors',
cache: 'default'
};
const myRequest = new Request(url, requestObj);
fetch(myRequest)
.then(response => response.json())
.then(jsonObj => {
//do stuff with whatever was returned in the json object
});
If you have an older browser that doesn't support JavaScript at the ES6 version or later, then you can revert to the older XMLHttpRequest.
Ref XMLHttpRequest Docs:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
var url = 'https://www.example.com/api/content';
var getRequest = new XMLHttpRequest();
getRequest.open('GET', url, false);
getRequest.setRequestHeader("Authentication", ''basic <insert base64 encoded basic auth string>'');
getRequest.send(null);
if (getRequest.status == 200){
var textData = req.responseText;
// do stuff with the response data
}
That is a VERY basic use case for XMLHttpRequest... you should definitely read the docs on setting it up to use call back functions instead of operating synchronously as in the example above. But this should give you enough detail to move forward
I'm using VueJS for an app I am building. The server I have is written in Golang and has been set to accept CORS. In the app, in one of my components, searchBar, I have set it to fetch some data before it is created.
var searchBar = {
prop: [...],
data: function() {
return { ... };
},
beforeCreate: function() {
var searchBar = this;
axios.request({
url: '/graphql',
method: 'post',
data: {
'query': '{courses{id, name}}'
}
})
.then(function(response) {
searchBar.courses = response.data.data.courses;
});
},
methods: { ... },
template: `...`
};
Axios works perfectly here. It gets the data I need. searchBar has a button which causes it to emit an event which is then picked up by another component, searchResults. Upon receiving the event, searchResults will fetch some data.
var searchResults = {
data: function() {
return { ... }
},
mounted: function() {
var sr = this;
this.$bus.$on('poll-server', function(payload) {
var requestData = {
url: '/graphql',
method: 'post',
data: { ... },
...
};
...
axios.request(requestData)
.then(function(response) {
console.log(response);
}
);
});
},
template: `...`
};
Note that my Axios request call is now inside a callback function. When this call is performed, I receive a CORS error:
Access to XMLHttpRequest at 'http://127.0.0.1:9000/graphql' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
My server is located at http://127.0.0.1:9000, with the client in http://127.0.0.1:8080. Here is the content of the OPTIONS request of the second request call.
For comparison, here is the request header of the first request call (that works!).
I have already set my Golang server to support CORS via go-chi/cors. This is how set it up.
router := chi.NewRouter()
...
// Enable CORS.
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"POST", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300,
})
router.Use(
render.SetContentType(render.ContentTypeJSON),
middleware.Logger,
middleware.DefaultCompress,
middleware.StripSlashes,
middleware.Recoverer,
cors.Handler,
)
router.Post("/graphql", gqlServer.GraphQL())
return router, db
What is causing the error I am having and how can it be solved?
This CORS error is expected. The CORS plugin you are using does request filtering for you. If you look at the list of allowed headers, you can see it's missing the header called snb-user-gps-location that you are trying to send in your axios call.
Either add that header to the allowed list, or don't send it from the front end.
I still suspect the go-chi CORS setup. I would suggest looking at setting up CORS by hand. It's not that difficult. This page will get a basic setup: https://flaviocopes.com/golang-enable-cors/
If that works with your nested API setup, we can then work backwards to determine the go-chi config issue.
Update:
I would also investigate the other middleware steps - commenting out all non-essential ones.
Middleware handlers normally inspect the r *http.Request or write headers to the w http.ResponseWriter and then the final handler will write to the response body. But throughout the middleware chain the following header/body write flow should look like one of these two flows:
Success:
w.Header().Set(...) // headers
w.Write(...) // body
Note the above flow will issue an implicit http status code write, to keep headers appearing first and body second:
w.Header().Set(...) // headers
w.WriteHeader(http.StatusOK) // implicit success http status code
w.Write(...) // body
Failure:
In the event of reporting a runtime error, the flow should be:
w.Header().Set(...) // headers
w.WriteHeader(http.StatusInternalServerError) // some unrecoverable error
w.Write(...) // optional body
The reason I bring this up, I've seen 3 types of bugs which mess up this flow:
bad middleware handlers write headers after the body causing client confusion
calling http.Error thinking that stops the API dead - instead of returning immediately after the http.Error and ensuring no subsequent middleware handlers are called
write the same header twice. Rewriting headers in a subsequent handler will cause the client to see the last version (thus clobbering any previous versions)
So to fully trace things, I would log.Println all header/body writes for your API to ensure the above flow is correct and no intended values are being overwritten.
Consider this sample index.html file.
<!DOCTYPE html>
<html><head><title>test page</title>
<script>navigator.serviceWorker.register('sw.js');</script>
</head>
<body>
<p>test page</p>
</body>
</html>
Using this Service Worker, designed to load from the cache, then fallback to the network if necessary.
cacheFirst = (request) => {
var mycache;
return caches.open('mycache')
.then(cache => {
mycache = cache;
cache.match(request);
})
.then(match => match || fetch(request, {credentials: 'include'}))
.then(response => {
mycache.put(request, response.clone());
return response;
})
}
addEventListener('fetch', event => event.respondWith(cacheFirst(event.request)));
This fails badly on Chrome 62. Refreshing the HTML fails to load in the browser at all, with a "This site can't be reached" error; I have to shift refresh to get out of this broken state. In the console, it says:
Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit.
"construct a Request"?! I'm not constructing a request. I'm using the event's request, unmodified. What am I doing wrong here?
Based on further research, it turns out that I am constructing a Request when I fetch(request, {credentials: 'include'})!
Whenever you pass an options object to fetch, that object is the RequestInit, and it creates a new Request object when you do that. And, uh, apparently you can't ask fetch() to create a new Request in navigate mode and a non-empty RequestInit for some reason.
In my case, the event's navigation Request already allowed credentials, so the fix is to convert fetch(request, {credentials: 'include'}) into fetch(request).
I was fooled into thinking I needed {credentials: 'include'} due to this Google documentation article.
When you use fetch, by default, requests won't contain credentials such as cookies. If you want credentials, instead call:
fetch(url, {
credentials: 'include'
})
That's only true if you pass fetch a URL, as they do in the code sample. If you have a Request object on hand, as we normally do in a Service Worker, the Request knows whether it wants to use credentials or not, so fetch(request) will use credentials normally.
https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker
var networkDataReceived = false;
// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
return response.json();
}).then(function(data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches.match('mycache').then(function(response) {
if (!response) throw Error("No data");
return response.json();
}).then(function(data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
}).catch(function() {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
}).catch(showErrorMessage).then(console.log('error');
Best example of what you are trying to do, though you have to update your code accordingly. The web example is taken from under Cache then network.
for the service worker:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mycache').then(function(cache) {
return fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
})
);
});
Problem
I came across this problem when trying to override fetch for all kinds of different assets. navigate mode was set for the initial Request that gets the index.html (or other html) file; and I wanted the same caching rules applied to it as I wanted to several other static assets.
Here are the two things I wanted to be able to accomplish:
When fetching static assets, I want to sometimes be able to override the url, meaning I want something like: fetch(new Request(newUrl))
At the same time, I want them to be fetched just as the sender intended; meaning I want to set second argument of fetch (i.e. the RequestInit object mentioned in the error message) to the originalRequest itself, like so: fetch(new Request(newUrl), originalRequest)
However the second part is not possible for requests in navigate mode (i.e. the initial html file); at the same time it is not needed, as explained by others, since it will already keep it's cookies, credentials etc.
Solution
Here is my work-around: a versatile fetch that...
can override the URL
can override RequestInit config object
works with both, navigate as well as any other requests
function fetchOverride(originalRequest, newUrl) {
const fetchArgs = [new Request(newUrl)];
if (request.mode !== 'navigate') {
// customize the request only if NOT in navigate mode
// (since in "navigate" that is not allowed)
fetchArgs.push(request);
}
return fetch(...fetchArgs);
}
In my case I was contructing a request from a serialized form in a service worker (to handle failed POSTs). In the original request it had the mode attribute set, which is readonly, so before one reconstructs the request, delete the mode attribute:
delete serializedRequest["mode"];
request = new Request(serializedRequest.url, serializedRequest);
I am trying to query the App Store for information on a given app, however I keep getting the following error.
XMLHttpRequest cannot load https://itunes.apple.com/lookup?id=<some-app-id>.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://www.<some-website>.co.uk' is therefore not allowed access. The response had HTTP status code 501.
The code I'm using to execute the request is as follows.
Does anyone know where I may be going wrong?
var config = {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With',
}
};
$http.get("https://itunes.apple.com/lookup?id=<some-app-id>", config).success(
function(data) {
// I got some data back!
}
);
You can use $http.jsonp,
$http.jsonp("https://itunes.apple.com/lookup", {
params: {
'callback': 'functionName',
'id': 'some-app-id'
}
});
Where functionName is the name of your globally defined function in string form. You can redefine it in your module so that it has access to $scope.
Documentation
Edit: here's a plunker showing my successful approach roughly getting it into an AngularJS app:
http://plnkr.co/edit/QhRjw8dzK6Ob4mCu6T6Z?p=preview
Adding those headers to your server won't change what is happening. The cross origin headers need to be added by the iTunes API.
That is not going to happen, so what you need to do instead is to use JSONP style callbacks in your webpage. There is an example on the iTunes search API page.
http://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html
Note: When creating search fields and scripts for your website, you
should use dynamic script tags for your xmlhttp script call requests.
For example:
<script src="https://.../search?parameterkeyvalue&callback="{name of JavaScript function in webpage}"/>
Note the 'callback' parameter there. That is a function defined globally in your javascript on the page that will get called with the response from the request to the url in 'src'. That function puts the data into your page, or application. You'll have to figure out how.
It's a shame that the language used in this documentation is not clearer, because you must do some kind of JSONP style workaround since they don't have CORS enabled on their API.
If you need to dynamically add a script tag (fetching data once is not enough) you can try this tutorial:
Dynamically add script tag with src that may include document.write
The API in general is probably intended for use by backends (not affected by cross origin issues), not for client side fetching.
Using angular 2:
constructor(private _jsonp: Jsonp) {}
public getData(term: string): Observable<any> {
return this._jsonp.request(itunesSearchUrl)
.map(res => {
console.log(res);
});
}
I have an error reporting beacon I created using Google Apps script and it is published to run as myself and to be accessible to "anyone, even anonymous," which should mean that X-domain requests to GAS are allowed.
However, my browsers are now indicating there is no Access-Control-Allow-Origin header on the response after the code posts to the beacon.
Am I missing something here? This used to work as recently as two months ago. So long as the GAS was published for public access, then it was setting the Access-Control-Allow-Origin header.
In Google Apps Script:
Code.gs
function doPost(data){
if(data){
//Do Something
}
return ContentService.createTextOutput("{status:'okay'}", ContentService.MimeType.JSON);
}
Client Side:
script.js
$.post(beacon_url, data, null, "json");
When making calls to a contentservice script I always have sent a callback for JSONP. Since GAS does not support CORS this is the only reliable way to ensure your app doesn't break when x-domain issues arrive.
Making a call in jQuery just add "&callback=?". It will figure everything else out.
var url = "https://script.google.com/macros/s/{YourProjectId}/exec?offset="+offset+"&baseDate="+baseDate+"&callback=?";
$.getJSON( url,function( returnValue ){...});
On the server side
function doGet(e){
var callback = e.parameter.callback;
//do stuff ...
return ContentService.createTextOutput(callback+'('+ JSON.stringify(returnValue)+')').setMimeType(ContentService.MimeType.JAVASCRIPT);
}
I've lost a couple of hours with the same issue. The solution was trivial.
When you deploy the script as webapp, you get two URLs: the /dev one and the /exec one. You should use /exec one to make cross domain POST requests. The /dev one is always private: it requires to be authorized and doesn't set *Allow-Origin header.
PS.: The /exec one seems to be frozen — it doesn't reflect any changes of code until you manually deploy it with a new version string (dropdown list in deploy dialog). To debug the most recent version of the script with the /dev URL just install an alternative browser and disable it's web-security features (--disable-web-security in GoogleChrome).
Just to make it simpler for those who are only interested in a POST request like me:
function doPost(e){
//do stuff ...
var MyResponse = "It Works!";
return ContentService.createTextOutput(MyResponse).setMimeType(ContentService.MimeType.JAVASCRIPT);
}
I stumbled upon the same issue:
calling /exec-urls from the browser went fine when running a webpage on localhost
throws crossorigin-error when called from a https-domain
I was trying to avoid refactoring my POST JSON-clientcode into JSONP (I was skeptical, since things always worked before).
Possible Fix #1
Luckily, after I did one non-CORS request (fetch() in the browser from a https-domain, using mode: no-cors), the usual CORS-requests worked fine again.
last thoughts
A last explanation might be: every new appscript-deployment needs a bit of time/usage before its configuration actually settled down at server-level.
Following solution works for me
In Google Apps Script
function doPost(e) {
return ContentService.createTextOutput(JSON.stringify({status: "success", "data": "my-data"})).setMimeType(ContentService.MimeType.JSON);
}
In JavaScript
fetch(URL, {
redirect: "follow",
method: "POST",
body: JSON.stringify(DATA),
headers: {
"Content-Type": "text/plain;charset=utf-8",
},
})
Notice the attribute redirect: "follow" which is very very important. Without that, it doesn't work for me.
I faced a similar issue of CORS policy error when I tried to integrate the app script application with another Vue application.
Please be careful with the following configurations:
Project version should be NEW for every deployment.
Execute the app as me in case you want to give access to all.
Who has access to the app to anyone, anonymous.
Hope this works for you.
in your calling application, just set the content-type to text/plain, and you will be able to parse the returned JSON from GAS as a valid json object.
Here is my JSON object in my google script doPost function
var result = {
status: 200,
error: 'None',
rowID: rowID
};
ws.appendRow(rowContents);
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
and here I am calling my app script API from node js
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'text/plain'},
body: JSON.stringify({param1: value, param2:value})
};
const response = await fetch(server_URL, requestOptions);
const data = await response.json();
console.log(data);
console.log(data.status);
My case is different, I'm facing the CORS error in a very weird way.
My code works normally and no CORS errors, only until I added a constant:
const MY_CONST = "...";
It seems that Google Apps Script (GAS) won't allow 'const' keyword, GAS is based on ES3 or before ES5 or that kind of thing. The error on 'const' redirect to an error page URL with no CORS.
Reference:
https://stackoverflow.com/a/54413892/5581893
In case this helps all any of those people like me:
I have a .js file which contains all my utility functions, including ones which call a GAS. I keep forgetting to clear my cache when I go to test updates, so I'll often get this kind of error because the cached code is using the /dev link instead of the /exec one.