Bugs can be loved too

By
/13.01.26
Bugs können auch geliebt werden
When bugs become beloved habits.
I've always lived in old houses, and there's something I have a guilty fondness for: things that stopped working as they should and started working with some little trick, a workaround that acts like a secret code among those of us who know the artifact. Doors and windows that look stuck at first glance, but you just have to make a certain move and the magic happens; toilet flushers that keep running unless you press the button in a specific way; or keys that only one person can turn. The same thing happens in our applications, but it's not as charming. People start using something that works but has a flaw in its implementation. Then changing it can become difficult and even dangerous because we might break something that many users are already relying on.
Recently, we had a problem like this in one of the applications we were working on. Users could manage different types of documents and folders, where they could nest more folders and documents inside them. However, we had an error in how we were handling the resource routes:
  • GET /documents/:key/edit pointed to document editing
  • DELETE /documents/:key pointed to document deletion
  • POST /documents/:key pointed to creating a new document
But:
  • GET /documents/:key, instead of going to the document with ID :key, was fetching the folder with ID :key
This means that, for some reason, we had lost our proper RESTful routes. The solution was pretty straightforward: we simply changed the way to access a folder:
  • GET /documents/browse/:dirpath
With this change, we could now have the proper RESTful routes:
  • GET /documents/:key pointed to fetching the document with ID :key
We wrote the corresponding tests (everything worked perfectly), deployed to staging, did QA, and finally deployed to production.
But as I warned you: people fall in love with our features, but also with our bugs and our technical debt. And the complaints didn't take long to arrive: many people had links to documents and folders using the old route structure, and those links were in important documents like operation protocols and procedures. By adding /browse for folders, all folder links were broken, since now, it was trying to find a document with ID :key, but that :key was actually a folder name.
To fix this, we had to find a change that would ensure backward compatibility with the old links. One possible solution would be to add a temporary redirect for the old links pointing to folders:
  • GET /documents/:key → /documents/browse/:key
However, this isn't so simple and doesn't look great in the code, because the user might actually be trying to access a document. So we could try to look for a document with that ID and, if we don't find it, redirect the user to the corresponding folder:
  • GET /documents/:key → /documents/browse/:key if no document with that ID is found
This works and is an acceptable solution, although it requires leaving some comments in the code explaining why that redirect exists.
  def show
  if @document = Document.find_by(id: params[:key])
    serve @document
  else
    # Redirect the user to the corresponding folder if no document with that ID is found
    # This is necessary because old links may point to folders instead of documents
    redirect_to "/documents/browse/#{params[:key]}"
  end
end
However, we went with a different solution. Since our old (now broken) links pointed to folders, we decided the best approach was to leave that route as it was and, instead, access documents through a special route:
  • GET /documents/view/:key → View document with ID :key
  • GET /documents/:key → View folder with ID :key
This allowed us to keep responsibilities separate and avoid confusion in the code. However, we lost the ability to have a resource with proper RESTful routes:
  # routes.rb
resources :documents, except: %i[show index]
get "documents/view/:id" => "documents#show", as: :document
It's not such an ugly solution nor such a serious problem. And luckily we were able to resolve it quickly. But we learned an important lesson: changing a current implementation that's already working and being used by users can have unexpected consequences, even if there's no error in the code. Whether it's a bug or technical debt, users have already adopted it without realizing something was wrong, and we have to always prioritize their experience over any other technical consideration.