Hacker News new | past | comments | ask | show | jobs | submit login
Offline-Friendly Forms (mxb.at)
236 points by mxbck on Aug 23, 2017 | hide | past | favorite | 63 comments



> If a user is currently offline, we’ll hold off submitting the form for now

Be careful not to completely rely on navigator.onLine for this as your next request could be the first to fail, so you need to deal with submission errors and timeouts too (or safer: always put the data in local storage and remove once you have confirmation of a successful save).

The article seems good overall though, I can forgive leaving out some of the nitty-gritty like this to keep the main points clear.


On top of that, I'd consider adding a bit of idempotency to the form submission -- to make sure that if the request did in fact succeed even though you did not get the confirmation of a success, subsequent retry/resubmit attempts won't result in duplicate entries.

A rough way to do this would be to add a hidden UUID value in the form data that is generated once up front and used by both the client and the server to ensure that the result of the first successful request can be cached and re-returned.


The approach of using versioned objects with Etag/If-None-Match and appropriate responses works rather well: http://www.clairereynaud.net/blog/adding-offline-mode-to-you...


Agreed, although if you look at the code, they do in fact do that - it's only removed from localStorage after receiving a 200 status response. Any other status code will leave the data in localStorage and display an error message.


The problem then becomes what happens when the server receives the request, processes and stores the data, but loses connection before the client can receive the 200. In this case, the client will assume the submission failed and will retry the submission - creating two records.


You really have to pick whether you want possible additional copies or possibly zero copies. This isn't really a problem with this particular implementation. I think it's interesting to have the possibility of additional copies versus the standard web form behavior of at-most-one copy.


> You really have to pick whether you want possible additional copies or possibly zero copies.

You really don't. You can, as mentioned above, have a hidden UUID field in the form that's used serverside to deduplicate submissions within a given timeframe.


That's not choosing between "at least once" and "[exactly] once", but rather how to deal with "at least once".


Wouldn't you also need to regenerate CSRF tokens somehow so that the second submission doesn't reuse the initial CSRF token?


That wouldn't be a successful request... if the user gets an error from your application, they know to try again.

(Or more likely and more on-topic, for the offline-friendly form if the application gets the error first, the application could be smart enough to try again...)

The UUID doesn't guarantee anything other than capability to prevent duplicate records from duplicate submissions. Something else has to be responsible to make sure the submission is not abandoned without user input, unless a 200 or other successful status is received.


Depends on how often your CSRF tokens expire. For example, Django keeps the same token for the entire session by default IIRC.


There's no reason to expire CSRF tokens after each request.


Standard web forms don't guarantee at-most-once. If the connection fails when the response is sent, the user thinks it failed and retries, submitting another request and causing another record creation.

Take a look at how Stripe guarantees at-most-once execution: https://stripe.com/blog/idempotency.


Another pitfall is that navigator.onLine can be true even though the device has no access to the Internet.


Improving UX is important here, but keep in mind `navigator.onLine` is prone to false positives when it returns true in Chrome and Safari.

https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOn...

If you need to support more browsers it isn't difficult. Just make a request to a small static asset or a health route and keep track of the online status yourself. You could even combine this with `navigator.onLine` for the best of both worlds.


I would just submit the form and check if it had a successful response. It can still be unreliable on a flaky network (the exactly once issue), but anyway it's cheaper on resources.


Is listening for the event really expensive? Honest question. We update the UI when it happens, so I don't think relying on the success/failure of network requests is reasonable for us anyways.


Yea, I would do the same. I was just speaking in terms of online awareness.


It's a shame the API's aren't better for this situation, pinging a static asset to determine network status is not good for battery life on smartphones if it's going to occur for any length of time.


The real problem is that there is no way to reliably know just how far your connection goes, without pinging. You might be able to connect to URLs on a university WAN, but not to the rest of the internet, for example. In this situation, as far as uni websites are concerned, you are online, but for Youtube, you are effectively offline. The concept of "online" does not have a trivial boolean definition.


It also badly impacts UX, because on slow networks the request-response roundtrip even for small static asset may be very long, so that the time needed for the feedback in the form will be in fact doubled.


Do you have any information to support this claim about battery drain? Genuinely interested, not trying to negate what you're saying.


I don't have any citations, it's just my understanding that switching on the mobile radio is a major battery drain, and should be avoided as much as possible.


As a user, instead of the "we will send the data once you are online" message, I would prefer "you seem offline, please try again once you are online" kind of approach. I would not like to rely on a background worker on a mobile device and I would not know whether my form has been sent, a timeout occured, what happens if I refresh the tab, what happens in a poor connection (seems online but can't connect to internet). Therefore, I would like to be just notified about the connection issue and try again once I got out of the metro.


If this tied into the platforms' notification systems, it could get around that; a notification appears for an unsubmitted form, and hangs around until it has successfully gone through, possibly with another notification of success, or failure after a certain timeout.


I'm actually expecting a Service Worker solution using Background Sync feature, like the description of the problem in this article: https://developers.google.com/web/updates/2015/12/background...


Seems to have already shipped in Chrome. And making progress on Firefox:

https://bugzilla.mozilla.org/show_bug.cgi?id=1217544


Be careful with sensitive information (not just passwords):

https://stackoverflow.com/questions/7859972/storing-credenti...

Not storing that kind of stuff will need prompts to re-enter it too.


If you delete it as soon as the user comes online, wouldn't their information be safe? No one else would've been able to get the information because they were offline, right? And so it's not still saved if the user exits the webpage before they return online, you can use SessionStorage.


Any situation where a password is persisted, especially in plaintext, can present a security risk. It is one of those things where you don't just guess that it is "safe enough" unless you have thoroughly proven it to be so.


It's more that you put the password on the disk in unencrypted format without telling the user. For instance, chrome stores them in sqlite databases which you can just open and select from:

http://i.imgur.com/zauv4sK.png

Your disk could be encrypted, but not everyone's will be. It's better to just localStorage as it was intended.

Plus if you introduce a bug later down the line, you might not prune older localStorage entries, meaning they will stay there for much longer than you want. AND the user may not revisit your site ever again after going offline, which doesn't give you an opportunity to prune it.


So encrypt the password with your public key before storing it and decrypt it on the server?


So ask the user if they're cool with it being unencrypted, and you're all good? How would you encrypt it though, since you're offline?


No one's going to have a lot of confidence if you ask them to store your sensitive info in plaintext. Just don't do it. The convenience or UX isn't worth it.


No. They can copy the data to another location to send later.


They as in an attacker? But the user is offline. Explain more please.


You can have a malware that copies anything stored in the local storage to its own database and transmits to a server as soon as the user goes back online. local storage is just as vulnerable to being read by JavaScript as cookies are.

local storage can be read using JavaScript from the same domain if you control all the JS on the domain, then this shouldn't be a problem. But if any other code is executed (i.e. via injection), they will be able to access the local storage


A persistent threat can stay on a device even when it's offline.

Nobody here is saying that an attacker can easily access your domain's localstorage, but just expressing the sentiment that "storing plaintext passwords is bad in almost any case".

Just like you can store plaintext passwords in your application database, and theoretically they are safe, but if a bad guy gets in your users are screwed, not just on your site but on others.


Exactly. In the very worst case, if local storage is to be used for storing password, it should be stored with asymmetric cryptography so that encryption is done with public key, but decryption can only be done with private key which is stored in the server (And not on the client). With a proper key rotation scheme, this could be an OK solution.


In your response, who is the "they"?


attacker who might have gained access to same domain privileges through code injection.


> If you dont want to use ajax to send your form submission, another solution would be to just repopulate the form fields with the stored data, then calling form.submit()

What are the pros and cons between these 2 approaches?


It mostly depends on the server receiving the form submission. If your server is setup to receive form posts, constructing a form post in javascript would be really cumbersome, so just repopulating the form and resubmitting would be the easiest. But if your server handles JSON, you can just take your stored data and post it right to the server.


Using jquery, the following looks exactly like the submitted form to the server side:

    $.ajax({
           type: "POST",
           url: url,
           data: $("#form").serialize(), // serializes the form's elements.
           success: function(data)
           {
               //do stuff
           }
    });
Instead of serializing the form, jquery can also just serialize a flat js object. I wouldn't call that complicated enough to make a distinction. (it's probably really easy in modern plain js too)


In your example, you'll also need to change the headers because $.ajax will set content type to application/json, which wouldn't work if your server is setup to receive x-www-form-urlencoded or multipart/form-data.

Starting to get complicated now, might be easier to just do $("#form").submit().


right, forgot to change the header.

Sure, it's more complicated than $("#form").submit(). But it's close enough that other considerations become more important (e.g. do I want a page reload, do I have to populate the form first etc)


Yeah, but the returned data is completely different. On one case, you'd expect a new page which the browser would render. In the other, a response which is handled via JS.


Tried something like that in Django, but needed another page of JS to get CSRF tokens to work. Its not always as simple as the first suggestion.


Will that work for file uploads?


You could also use caching headers to know whether the client is about to update an object older than the current on on the server in the case where the form is being used to "edit" an object. Hopefully the server would respond to the If-None-Match header with the object version and give you a 304: Not Modified response... yay, you're free to upload the data! Otherwise you might want to notify the user that their changes are out-dated when the server returns the newer version of the object.


How did I not know about navigator.onLine and the online/offline events!? Before reading this, I figured that would be the hardest part.


How do you signal to the user when the form has been commited to a server? Would you have to create a queue view that shows all the pending modifications and send push notifications when an item in the queue has been committed?


This is a super useful concept, thanks for sharing this.


This is amazingly useful.


Ok, let me just open up my developer console using F12 and check the Offline box on the network panel. So glad I took the extra 20 seconds to do that before reading part of the article and navigating away from this "distraction".


> window.addEventListener('online', () => this.checkStorage());

Every time I see this double function safety pattern a small part of my JS heart dies.


Eh, you prefer using bind() or something? Because that still creates another function.


What's the better alternative?


Using bind and saving a reference to the bound function one level up, not creating a new one each time


If I understand their concerns correctly, they would prefer this:

> window.addEventListener('online', this.checkStorage);


That wouldn't work, you'd need to at least use bind() so that the `this` keyword within `this.checkStorage` references the right thing, e.g.

    window.addEventListener('online', this.checkStorage.bind(this));


The language can do that for you:

    class Foo {
      checkStorage = () => {
        // Do check storage stuff here 
      }
    }
And the language will take care of binding the function. (Really, it's just doing `this.checkStorage = this.checkStorage.bind(this)` in the constructor).


That's not in 'the language' yet :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: