3 Toughest Issues in Frontend Development and How to Debug Them

How do you debug when you can’t seem to catch the bug? During our work, we came across a complex issue involving CORS, cache invalidation, and naming – a rare find in frontend development. We present our path to the solution. 

Debugging software issues can be both challenging and rewarding. However, when not done systematically, it is easy to lose yourself in the maze of errors, symptoms, and possible solutions. 

Sometimes, tracking that one elusive issue can make an engineer feel like they should consider a career switch to forensics. The story we’re about to share is a perfect example of this – a real-world debugging experience that revolved around one frustrating CORS (Cross-Origin Resource Sharing) issue. 

The idea is to demonstrate the thought process and methods used to identify and ultimately resolve the problem. Whether you are new to frontend development or an experienced senior, there’s something to learn from this journey.

What is CORS?

To follow this story, it’s important first to understand what CORS is and what’s its purpose.

Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers to restrict web pages from making requests to a different domain than the one that served the original web page. It is a set of HTTP headers that allows or denies web browsers to execute cross-origin requests based on the policy defined by the server. 

CORS is crucial for web security and helps prevent potential security vulnerabilities, such as cross-site request forgery (CSRF) and cross-site scripting (XSS).

A bug in the image editor

With the theoretical part covered, we can delve into the issue. 

During our project work, we were developing a content management application for a client. The application also includes an image editor, used for attaching an image to the content. On the image editor page, users are able to select one of the previously uploaded images for additional editing, for example, cropping. The selection is made by fetching the image via the fetch method, and the image is used to populate the canvas element later on.

This was the part of the app that contained a bug.

The mysterious report with a CORS error

It all started with a user report stating that the image could not be selected. When something like this happens, the first thing to do is to check the error monitoring system for errors. In our case, the logs were showing a CORS error. 

This is where the real mystery began, as our app had been up and running for some time, and the CORS should not have been the problem. At least, that is what we thought.

When reproducing the bug doesn’t work

After not being able to reproduce the issue in an isolated environment, we jumped into the production environment to examine the issue. A perplexing challenge presented itself – the bug was not reproducible. 

Everything appeared to work fine from the developer’s standpoint; the problem couldn’t be reproduced by us or the user anymore. Nevertheless, the issue persisted, and it resurfaced a week later, leaving both the user and us baffled.

To get to the answer, you need to be asking the right questions

A key breakthrough came through the process of asking questions and retrieving user feedback combined with thorough testing. 

Reaching out to users is always a great idea. Only when we attempted to replicate the entire workflow fully did the error finally surface. We discovered that the error occurred only after the user completed a series of specific steps. The end result was the error present in our console:

	No 'Access-Control-Allow-Origin' header is present on the requested resource.

Isolating the issue

After defining the right steps to reproduce the bug in an isolated environment, the key thing to learn is the minimal set of steps that lead to the bug. 

When we eliminated all of the unnecessary steps, the bug was revealed. It was reproducible if the user viewed the image somewhere in the app before selecting it in the image editor.

Knowing that the prerequisite for reproducing the bug was viewing the image before selection, the next step was to jump into developer tools, the network tab, and examine the requests leading to the bug. How was it possible that the image that we were displaying suddenly broke CORS?

Uncovering the cache problem

When we were inspecting the request that failed with the CORS error, one thing stood out – the “Provisional headers are shown” warning message in the developer console. This meant that the browser returned the request from the cache instead of creating a new request.

The question now was – how come the request returned from the cache did not trigger the CORS issue upon the initial request? We tried disabling the cache to see if the issue was still reproducible, and you can probably already guess it – it wasn’t.

Understanding the core issue

The key revelation was that the response headers differed when fetching the image via an image tag:

	<img src="image.jpg" />

As compared to using JavaScript via a fetch method:

	fetch("image.jpg");

Notably, the

	Access-Control-Allow-Origin: *

header was absent in responses when fetched via an image tag.

This disparity in response headers meant that when an image was initially rendered on the UI and subsequently fetched via JavaScript, it became cached with the response header:

	Cache-Control: public, max-age=31557600

This indicated a cache duration of one year. When a JavaScript fetch call attempted to access the cached image, the missing

	Access-Control-Allow-Origin: * 

header triggered the CORS error.

The solution

In the world of software development, cache invalidation is usually tricky to handle. There are multiple ways of solving this problem. Let’s go through some of them.

If headers for the fetch method for fetching the image via JavaScript were updated to contain Cache: no store, the problem would be resolved. The same result would be accomplished by adding a timestamp as a query parameter to the request. However, this method comes with a downside – this request would never be cached, meaning that the next time the app fetches an image like this, the request would also be sent to the backend, even if the valid request is already present in the cache.

The more correct approach would be to implement a query parameter that is constantly the same. This brings us to another complex issue in the development world – naming. How to name such a parameter? For example, adding provisional: true to the request from fetch would ignore the cache for the first time but would return the cached request every other time.

The best approach requires some server setup, or to be exact, the ability to add specific response headers. This approach includes adding a header to the response:

	Vary: Accept

This way, the browser knows when to ignore the cached request. The Vary header tells the browser that the requests may vary depending on the value that you set in the header.

For example, the request that originates from an image tag will have the Accept header value set to image/avif, image/webp, image/apng, image/svg+xml…, and if you’re using fetch, you can set the value to image/jpeg. Based on that, the browser will cache the request depending on the URL and the value of the Accept header.

In our example, we went with the addition of a query parameter:

	provisional: true

We decided on this approach because we were using an Amazon S3 bucket for serving the images, and Vary is not a user-configurable header. A different file storage service might allow setting the Vary response header. If it does, that would be the preferred solution.

Underlying concepts

As the first letter in REST (representational state transfer) implies, the requested resource can be represented in many different ways. The same API endpoint can return a different representation of the same resource, depending on the way the resource is requested. 

In our example, the same image was returned as content, but REST allows for much more. For example, the image can be requested with different values in the Accept header, like image/png or image/webp, and the API should then be able to return the image in other formats.

One of the common uses of the Content Negotiation principle is to apply the Accept-language header based on which the resource will be translated. Some browsers will send this header by default, filled with the value from the browser settings. The Vary header also plays a role in this, as we’ve seen in the example provided.

Mystery resolved

This debugging journey teaches us the importance of thorough investigation, asking the right questions, and understanding the intricacies of the technologies we work with. 

Mysterious issues can be tricky, but with patience and persistence, combined with a systematic debugging approach, they can be overcome. This at least applies to the CORS and cache issues. The naming will always be tricky.