workbox offline response from IDB instead of cache - response

I am building an vueJs application with a service worker. I decided to use Workbox with an InjestManifest method to had my own routes.
on fetch when online :
1- answer with the network
2- wrtting body to IDB (through localforage)
3- send back the response
here everything is working perfectly, the sw intercepts the fetch and come back with an appropirate response, IDB contains rigth details.
response sent back to fecth when online:
Response {type: "cors", url: "http://localhost:3005/api/events", redirected: false, status: 200, ok: true, …}
the issue is when I go offline.
my intention id to connect to Locaforage and retrieve the content and build a response.
The issue is that this response is not considered as appropriate by Fetch who then reject it. Console.log confirms that the .catch in sw is working but it looks like the response it sends is rejected.
here is the console.log of the response I am sending back to fetch when offline;
Response {type: "default", url: "", redirected: false, status: 200, ok: true, …}
I do not know if fetch is not happy becasue the url of the repsonse is not the same as on the request but workbox is supposed to allow responding with other resposnes than the ones coming from cache or fetch.
here is the code
importScripts('localforage.min.js')
localforage.config({
name: 'Asso-corse'
})
workbox.skipWaiting()
workbox.clientsClaim()
workbox.routing.registerRoute(
new RegExp('https://fonts.(?:googleapis|gstatic).com/(.*)'),
workbox.strategies.cacheFirst({
cacheName: 'googleapis',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 30
})
]
})
)
workbox.routing.registerRoute( new RegExp('http://localhost:3005/api/'), function (event) {
fetch(event.url)
.then((response) => {
var cloneRes = response.clone()
console.log(cloneRes)
cloneRes.json()
.then((body) => {
localforage.setItem(event.url.pathname, body)
})
return response
})
.catch(function (error) {
console.warn(`Constructing a fallback response, due to an error while fetching the real response:, ${error}`)
localforage.getItem(event.url.pathname)
.then((res) => {
let payload = new Response(JSON.stringify(res), { "status" : 200 ,
"statusText" : "MyCustomResponse!" })
console.log(payload)
return payload
})
})
})
workbox.precaching.precacheAndRoute(self.__precacheManifest || [])
I am really stuck there as all documentation on workbox relates to leveraging cache. I am leveraging localforage as it supports promises which is what is required to make offline capability working.
Thanks

Your catch() handler needs to return either a Response object, or a promise for a Response object.
Adjusting the formatting of your sample code a bit, you're currently doing:
.catch(function (error) {
console.warn(`Constructing a fallback response, due to an error while fetching the real response:, ${error}`)
localforage.getItem(event.url.pathname).then((res) => {
let payload = new Response(JSON.stringify(res), { "status" : 200 , "statusText" : "MyCustomResponse!" })
console.log(payload)
return payload
})
})
Based on that formatting, I think it's clearer that you're not returning either a Response or a promise for a Response from within your catch() handler—you're not returning anything at all.
Adding in a return before your localforage.getItem(...) statement should take care of that:
.catch(function (error) {
console.warn(`Constructing a fallback response, due to an error while fetching the real response:, ${error}`)
return localforage.getItem(event.url.pathname).then((res) => {
let payload = new Response(JSON.stringify(res), { "status" : 200 , "statusText" : "MyCustomResponse!" })
console.log(payload)
return payload
})
})
But, as mentioned in the comments to your original question, I don't think that using IndexedDB to store this type of URL-addressable data is necessary. You can just rely on the Cache Storage API, which Workbox will happily use by default, when storing and retrieving JSON data obtained from an HTTP API.

Related

Empty request body in onFetch method of serviceworker

I am trying to make my website work offline, and while offline any POST request should be saved to a local database. Currently I have this code in my serviceworker to catch any requests:
self.onfetch = (event) => {
switch (event.request.method) {
case 'GET':
return onGet(event);
case 'POST':
return onPost(event);
}
};
async function onPost(event) {
if (navigator.onLine){
return;
}
event.request.clone().formData().then((data) => {
console.log(data);
})
}
I am trying to read out the data from the request, but the console.log returns an empty object. When looking at the failed request in DevTools the FormData is available. Is it not possible to read out the body when intercepting the request?

How to dispatch a Paypal IPN to a Google Cloud function?

I've read here that it's possible to send an IPN directly to a Google cloud function. I have my Google Cloud functions running on Firebase on an index.js file.
I've set up my Paypal buttons to send the IPN to a page on my webapp.
Here is an example of one of the functions I'm running off Google Cloud Functions/Firebase:
// UPDATE ROOMS INS/OUTS
exports.updateRoomIns = functions.database.ref('/doors/{MACaddress}').onWrite((change, context) => {
const beforeData = change.before.val();
const afterData = change.after.val();
const roomPushKey = afterData.inRoom;
const insbefore = beforeData.ins;
const insafter = afterData.ins;
if ((insbefore === null || insbefore === undefined) && (insafter === null || insafter === undefined) || insbefore === insafter) {
return 0;
} else {
const updates = {};
Object.keys(insafter).forEach(key => {
updates['/rooms/' + roomPushKey + '/ins/' + key] = true;
});
return admin.database().ref().update(updates); // do the update}
}
return 0;
});
Now question:
1) I want to add another function to process IPN from Paypal as soon as I have a transaction. How would I go about this?
I'll mark the answer as correct if solves this first question.
2) how would that Google cloud function even look like?
I'll create another question if you can solve this one.
Note I am using Firebase (no other databases nor PHP).
IPN is simply a server that tries to reach a given endpoint.
First, you have to make sure that your firebase plan supports 3rd party requests (it's unavailable in the free plan).
After that, you need to make an http endpoint, like so:
exports.ipn = functions.http.onRequest((req, res) => {
// req and res are instances of req and res of Express.js
// You can validate the request and update your database accordingly.
});
It will be available in https://www.YOUR-FIREBASE-DOMAIN.com/ipn
Based on #Eliya Cohen answer:
on your firebase functions create a function such as:
exports.ipn = functions.https.onRequest((req, res) => {
var reqBody = req.body;
console.log(reqBody);
// do something else with the req.body i.e: updating a firebase node with some of that info
res.sendStatus(200);
});
When you deploy your functions go to your firebase console project and check your functions. You should have something like this:
Copy that url, go to paypal, edit the button that's triggering the purchase, scroll down to Step 3 and at the bottom type:
notify_url= paste that url here
Save changes.
You can now test your button and check the req.body on your firebase cloud functions Log tab.
Thanks to the answers here, and especially to this gist: https://gist.github.com/dsternlicht/fdef0c57f2f2561f2c6c477f81fa348e,
.. finally worked out a solution to verify the IPN request in a cloud func:
let CONFIRM_URL_SANDBOX = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
exports.ipn = functions.https.onRequest((req, res) => {
let body = req.body;
logr.debug('body: ' + StringUtil.toStr(body));
let postreq = 'cmd=_notify-validate';
// Iterate the original request payload object
// and prepend its keys and values to the post string
Object.keys(body).map((key) => {
postreq = `${postreq}&${key}=${body[key]}`;
return key;
});
let request = require('request');
let options = {
method: 'POST',
uri : CONFIRM_URL_SANDBOX,
headers: {
'Content-Length': postreq.length,
},
encoding: 'utf-8',
body: postreq
};
res.sendStatus(200);
return new Promise((resolve, reject) => {
// Make a post request to PayPal
return request(options, (error, response, resBody) => {
if (error || response.statusCode !== 200) {
reject(new Error(error));
return;
}
let bodyResult = resBody.substring(0, 8);
logr.debug('bodyResult: ' + bodyResult);
// Validate the response from PayPal and resolve / reject the promise.
if (resBody.substring(0, 8) === 'VERIFIED') {
return resolve(true);
} else if (resBody.substring(0, 7) === 'INVALID') {
return reject(new Error('IPN Message is invalid.'));
} else {
return reject(new Error('Unexpected response body.'));
}
});
});
});
Also thanks to:
https://developer.paypal.com/docs/classic/ipn/ht-ipn/#do-it
IPN listener request-response flow: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNImplementation/
To receive IPN message data from PayPal, your listener must follow this request-response flow:
Your listener listens for the HTTPS POST IPN messages that PayPal sends with each event.
After receiving the IPN message from PayPal, your listener returns an empty HTTP 200 response to PayPal. Otherwise, PayPal resends the IPN message.
Your listener sends the complete message back to PayPal using HTTPS POST.
Prefix the returned message with the cmd=_notify-validate variable, but do not change the message fields, the order of the fields, or the character encoding from the original message.
Extremely late to the party but for anyone still looking for this, PayPal have made a sample in their JS folder on their IPN samples Github repo.
You can find this at:
https://github.com/paypal/ipn-code-samples/blob/master/javascript/googlecloudfunctions.js

How to received promises from backend in Firebase httpscallable().call()?

I've been playing around with stripe and would like to learn how to get ephemeral keys in the following way:
Back-end:
//Stripe API requirement for payment
exports.stripeEphemeralKey = functions.https.onCall((data, context) => {
const uid = context.auth.uid;
//Get values
admin.database().ref().child("users").child(uid)
.on("value", (snapshot) =>{
//Get user data
let user = snapshot.val()
//Log data
console.log("Create ephemeral key for:")
console.log(user)
//Create ephemeral key
stripe.ephemeralKeys.create(
{customer: user.customerid },
{stripe_version: '2018-11-08'}
)
.then((key) => {
console.log(key)
console.log("Succesful path. Ephemeral created.")
return "Testing"
})
.catch((err) => {
console.log("Unsuccesful path. Ephemeral not created.")
console.log(err)
return {
valid: false,
data: "Error creating Stripe key"
}
})
})
})
Client-side:
functions.httpsCallable("stripeEphemeralKey").call(["text": "Testing"]) { (result, error) in
print(result?.data)
}
I have tested this code by replacing the body of the stripeEphemeralKey with a simple "Testing" string and that returns just fine. But with the code above I just get Optional() back.
For testing I added lots of console logs. Firebase logs show the execution path gets to the "Succesful path. Ephemeral created." log, and furthermore I can actually see the ephemeral key I get back from stripe.
So, what is the proper correct way to get the ephemeral key in Swift for iOS using the onCall Firebase function?
The backend does what it should, but I can't seem to get the answer back.
Thank you.
The backend does not actually do what it should do. You're doing at least two things wrong here.
First, your callable function needs to return a promise that resolves with the value that you want to send to the client. Right now, your function callback isn't returning anything at all, which means the client won't receive anything. You have return values inside promise handlers, but you need a top-level return statement.
Second, you're using on() to read data from Realtime Database, which attaches a listener that persists until it's removed. This is almost certainly never what you want to do in a Cloud Function. Instead, use once() to get a single snapshot of the data you want to read, and act on that.
For my own reference, and those who might find this helpful:
//Stripe API requirement for payment
exports.stripeEphemeralKey = functions.https.onCall((data, context) => {
const uid = context.auth.uid;
return admin.database().ref().child("users").child(uid)
.once("value", (snapshot) =>{
console.log(snapshot.val() )
})
.then( (snap) => {
const customer = snap.val()
//Log data
console.log("Create ephemeral key for:")
console.log(customer)
//Create ephemeral key
return stripe.ephemeralKeys.create(
{customer: customer.customerid },
{stripe_version: '2018-11-08'}
)
.then((key) => {
console.log(key)
console.log("Succesful path. Ephemeral created.")
return {
valid: true,
data: key
}
})
.catch((err) => {
console.log("Unsuccesful path. Ephemeral not created.")
console.log(err)
return {
valid: false,
data: "Error creating Stripe key"
}
})
})
.catch( err => {
console.log("Unsuccesful path. Ephemeral not created.")
console.log(err)
return {
valid: false,
data: "Error gettting customerid"
}
})
})
The key seems to be to chain the initial database request with .then(), and do our our work chaining the returns uninterrupted as we use functions that return promises. In particular, placing my work-code inside the callback on the original admin.database().ref().once() function did not work for me.
I am new to this kind of programming, so someone who knows about this might the why better.

IBM DataPower - How to handle HTML Response from openurl?

I tried looking for the solution in the forum but I was unable to find something similar to what I'm trying to achieve. I have a gateway script in an MPG which kinda looks like this:
session.INPUT.readAsJSON(function (error, json) {
if (error){
throw error;
} else {
var SAMLResponse = json['SAMLResponse'];
var RelayState = json['RelayState'];
var urlopen = require('urlopen');
var options = {
target: 'https://************.com/e32d32der2tj90g8h4',
method: 'POST',
headers: { 'HEADER_NAME' : 'VALUE'},
contentType: 'application/json',
timeout: 60,
sslClientProfile: 'ClientProfile',
data: {"SAMLResponse": SAMLResponse, "RelayState": RelayState}
};
urlopen.open(options, function(error, response) {
if (error) {
session.output.write("urlopen error: "+JSON.stringify(error));
} else {
var responseStatusCode = response.statusCode;
var responseReasonPhrase = response.reasonPhrase;
response.readAsBuffer(function(error, responseData){
if (error){
throw error;
} else {
session.output.write(responseData);
console.log(responseData);
}
});
}
});
}
});
I'm doing a POST request and the response I get from the urlopen function is an HTML page, how to I display the contents of that page in my browser? I need that to initiate a process flow. am I going in the wrong direction here? what's the best way to POST to a URI and display it's response in DataPower?
with regards to my experience with DataPower, I just started learning, So I might not be familiar with many of the concepts.
Thanks in Advance!
session.INPUT.readAsJSON() would indicate that you are receiving JSON data as the input (from the POST).
Since you are building this in a Multi-Protocol Gateway (MPGW) you need to set the Response type to non-xml if the response is HTML and if there is no backend call being made (other than the url-open()) you also must set the skip-backside=1 variable.
Is the scenario as:
JSON HTTP Request -> [MPGW] -> url-open() -> Backend server --|
HTTP Response <-----------------------------------------|
Or:
JSON HTTP Request -> [MPGW] -> url-open() --| (skip-backside)
HTTP Response <------------------------|
If there is no backend call I would recommend building this in a XML Firewall (XMLFW) service instead and set it to "loopback" and non-xml.
If there is a backend and that is where you are sending your HTML from the url-open() then only MPGW Response type needs to be set to non-xml.
If it is the second option the you can just set the payload and headers in GWS and just call the target (https://************.com/e32d32der2tj90g8h4) as teh MPGW backside connection, no need for the url-open().

Pass custom data to service worker sync?

I need to make a POST request and send some data. I'm using the service worker sync to handle offline situation.
But is there a way to pass the POST data to the service worker, so it makes the same request again?
Cause apparently the current solution is to store requests in some client side storage and after client gets connection - get the requests info from the storage and then send them.
Any more elegant way?
PS: I thought about just making the service worker send message to the application code so it does the request again ... but unfortunately it doesn't know the exact client that registered the service worker :(
You can use fetch-sync
or i use postmessage to fix this problem, which i agree that indexedDB looks trouble.
first of all, i send the message from html.
// send message to serviceWorker
function sync (url, options) {
navigator.serviceWorker.controller.postMessage({type: 'sync', url, options})
}
i got this message in serviceworker, and then i store it.
const syncStore = {}
self.addEventListener('message', event => {
if(event.data.type === 'sync') {
// get a unique id to save the data
const id = uuid()
syncStore[id] = event.data
// register a sync and pass the id as tag for it to get the data
self.registration.sync.register(id)
}
console.log(event.data)
})
in the sync event, i got the data and fetch
self.addEventListener('sync', event => {
// get the data by tag
const {url, options} = syncStore[event.tag]
event.waitUntil(fetch(url, options))
})
it works well in my test, what's more you can delete the memory store after the fetch
what's more, you may want to send back the result to the page. i will do this in the same way by postmessage.
as now i have to communicate between each other, i will change the fucnction sync into this way
// use messagechannel to communicate
sendMessageToSw (msg) {
return new Promise((resolve, reject) => {
// Create a Message Channel
const msg_chan = new MessageChannel()
// Handler for recieving message reply from service worker
msg_chan.port1.onmessage = event => {
if(event.data.error) {
reject(event.data.error)
} else {
resolve(event.data)
}
}
navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2])
})
}
// send message to serviceWorker
// you can see that i add a parse argument
// this is use to tell the serviceworker how to parse our data
function sync (url, options, parse) {
return sendMessageToSw({type: 'sync', url, options, parse})
}
i also have to change the message event, so that i can pass the port to sync event
self.addEventListener('message', event => {
if(isObject(event.data)) {
if(event.data.type === 'sync') {
// in this way, you can decide your tag
const id = event.data.id || uuid()
// pass the port into the memory stor
syncStore[id] = Object.assign({port: event.ports[0]}, event.data)
self.registration.sync.register(id)
}
}
})
up to now, we can handle the sync event
self.addEventListener('sync', event => {
const {url, options, port, parse} = syncStore[event.tag] || {}
// delete the memory
delete syncStore[event.tag]
event.waitUntil(fetch(url, options)
.then(response => {
// clone response because it will fail to parse if it parse again
const copy = response.clone()
if(response.ok) {
// parse it as you like
copy[parse]()
.then(data => {
// when success postmessage back
port.postMessage(data)
})
} else {
port.postMessage({error: response.status})
}
})
.catch(error => {
port.postMessage({error: error.message})
})
)
})
At the end. you cannot use postmessage to send response directly.Because it's illegal.So you need to parse it, such as text, json, blob, etc. i think that's enough.
As you have mention that, you may want to open the window.
i advice that you can use serviceworker to send a notification.
self.addEventListener('push', function (event) {
const title = 'i am a fucking test'
const options = {
body: 'Yay it works.',
}
event.waitUntil(self.registration.showNotification(title, options))
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(
clients.openWindow('https://yoursite.com')
)
})
when the client click we can open the window.
To comunicate with the serviceworker I use a trick:
in the fetch eventlistener I put this:
self.addEventListener('fetch', event => {
if (event.request.url.includes("sw_messages.js")) {
var zib = "some data";
event.respondWith(new Response("window.msg=" + JSON.stringify(zib) + ";", {
headers: {
'Content-Type': 'application/javascript'
}
}));
}
return;
});
then, in the main html I just add:
<script src="sw_messages.js"></script>
as the page loads, global variable msg will contain (in this example) "some data".

Resources