This is story about how I learned to be a little bit less naive about the web and how it works. But it's also a story of user experience and what we owe our users in terms of error handling - even when it's not us that cause the problem.
It all started with a support email
A while ago I received an email from a customer of my website, Thankbox, with the following:
"Probably another case of someone not filling in the payment form right." I thought. I was just about to respond with my generic saved answer but then I looked at the email again.
Did they say they can click on the "Pay and Send" button? And nothing happens?
The payment form isn't working right
As a bit of context - my product, Thankbox, is an online group card service. People use it to set up a card for someone's occasion (e.g. their birthday) and collect messages. Then once they're ready to send it they get a simple form where they put their payment information and recipient email:
I want to point your attention towards the disabled Pay & Send button there. It's disabled because the Stripe payment info isn't filled out. This is the typical customer support I was used to dealing with - someone forgetting to enter their postal code, for example.
But this user was saying that they could click on the button and nothing happened when they did. "This shouldn't happen" I thought (as any good engineer who doesn't know what's happening would).
This is what should happen when you click the "Pay & send" button:
- The Thankbox recipient details (email and schedule time) are updated.
- A request is made to Stripe to process the payment and handle any verification.
- Once the payment is processed a confirmation message is shown and the user is navigated to another page.
Any error scenario that could happen along that path should surface an error on the form. I try to be meticulous about error scenarios and testing when it comes to payment code. I knew that Stripe errors would surface correctly after Step 2 and so would my own server errors.
So I replied to the user and asked for a screenshot to confirm that they were indeed seeing what they said they were.
I received a reply a few hours later and what the user was saying was true - it showed the full form filled out with the payment button enabled.
The payment form isn't sending information... What, how?
I had a look at my Stripe dashboard to find more information about the payment the user was trying to make. Sure enough it showed as incomplete.
Ok, at least I've not charged and not delivered. I also looked at my database and saw the user's updates weren't coming.
Now I asked myself the question - "How?" How is it that these requests aren't even coming through?
Error logging to the rescue
I had already dove into my error logs a couple of times trying to figure out what was going on with this but I didn't find much. That, I realized, was because I had really only put error logs around things I was expecting and assuming. Like the fact that I'd always be getting a JSON response back from request to my own server for example.
So I decided to put in a more generic handler to catch all errors and report them to my error logger.
catch (error) {
if (hasOwn(error, 'response')) {
this.errors = error.response?.data?.errors || {}
this.reportError(error.response?.data)
} else {
// New Logic here
this.reportError(error)
}
}
I then asked the user to try again and observed the logs. What I saw confused my innocent frontend soul:
Why was I getting a 403 HTML response back?
The relevation - the POST request is getting firewalled
I copied the HTML into a file, ran it and saw the following:
Aha, so an overzealous IT network decided to block the request before it even reached my server. It helpfully spits out this HTML response in return but, of course, my frontend code was expecting a JSON response. I had no idea I was ever going to get anything different.
Now that I knew what was happening I could at least try to handle it.
I quickly emailed the user back and let them know that their IT system was blocking the payment from coming through. I advised them to try on their home network or their mobile data and to also kindly ask their IT department if they wouldn't mind whitelisting my website.
But I also had to handle all future such errors.
The solution - just show users these errors, duh
If these errors are coming back from IT firewalls then I should just show them to my users. That way they can at least know that my website isn't broken, which is what everyone who had the issue up until this point must have been thinking.
I wrote up a little Axios interceptor that checks for 403 or 503 responses (I noticed firewalls using both) that contain HTML and then I stored that inside the axios response itself.
// Possibly a corporate network blocking a request and returning an HTML error page that we should display
if ((statusCode === 403 || statusCode === 503) && response?.headers['content-type']?.includes('text/html')) {
response['html-403'] = response?.data
}
Then, anywhere in my code where I needed to handle the possibility of this error I just added some code to check for the existence of html-403
and show it in a new window for the user to see - that's what showHtmlPage
does. Here is the example of how I handle it on the send page:
catch (error) {
const htmlForbiddenBody = error.response?.['html-403']
if (htmlForbiddenBody) {
showHtmlPage(htmlForbiddenBody)
showAlert({
message: 'Could not send Thankbox due to an external error.',
level: 'error',
})
}
}
This is a lot more common than you think
Ever since adding this I've not had another customer support email show up. I have noticed, however, that it's actually way more common than I thought. Because I now surface these types of errors as a separate type in my error logger I can see how common they are.
Around 10 of these happen every week. Most often it's users on locked down government or corporate networks. Occasionally it's universities, too. I'm now comfortable knowing that these users aren't getting a weird error where nothing happens on their page, but instead get to go annoy at their IT department.