Skip to main content

Avoid error shadowing

What is an error shadowing?

Error shadowing is a hiding of the original cause of the problem.

A common use case may be always returning a 404 HTTP error code in production, even when the real code is different.

This way you make it a little bit harder for a possible attacker to find security holes in your web application.

danger

A professional web attacker may easily exploit any kind of security vulnerabilities.

That is why it is so important to be extremely careful.

Please, constantly impove your security skills.

Securing Rails Applications and OWASP: Ruby on Rails Cheat Sheet are good places to revisit from time to time.

Also it is popular to rescue low-level exceptions to encapsulate internal details from the end-user.

The following example is taken directly from the Rails codebase:

def download_chunk(key, range)
instrument :download_chunk, key: key, range: range do
file_for(key).download(range: range).string
rescue Google::Cloud::NotFoundError
raise ActiveStorage::FileNotFoundError
end
end

As you can see, the original exception message is dropped and not reused anywhere.

When does error shadowing happen with services?

Using Convenient Service, you may encounter an error shadowing as well.

But as always, any approach may have advantages and disadvantages.

To illustrate that, check the service below:

class MainService
# ...
def result
return error(message: "Something went wrong") unless sub_service_result.success?
# ...
success
end

private

def sub_service_result
@sub_service_result ||= SubService.result
end
# ...
end

MainService looks innocent, but the problem starts to appear when you execute it.

result = MainService.result

result.success?
# => false

result.message
# => "Something went wrong"

result.message does NOT help to figure out what is actually wrong.

SubService is failing in reality, but it is NOT reflected in the outside world in any way.

If you don't want to intentionally confuse the service users or hide the internals from them, prefer to forward the original message.

class MainService
# ...
def result
return error(message: "Something went wrong (#{sub_service_result.message})") unless sub_service_result.success?
# ...
success
end
# ...
end

It is also OK to immediately return SubService result when you don't need to provide any additional text to its message, since it also avoids error shadowing.

class MainService
# ...
def result
return sub_service_result unless sub_service_result.success?
# ...
success
end
# ...
end

But the best option is to utilize the step macro.

It works as the previous example under the hood and when used frequently - helps to forget about error shadowing in the context of services completely.

class MainService
step SubService
# ...
def result
# This line is removed...
# ...
success
end
# ...
end
note

If you are not safisfied how step automatically prevents error shadowing - consider to create a plugin to extend/modify it.