When a Feature Is Turned Off, Answer 404 Not 403
A disabled reviews feature answered 404 instead of 403. The HTTP spec actually backs that choice, and one cheap gate makes it practical.
TL;DR
Returning 403 for a disabled feature confirms the resource exists, inviting attackers to keep probing. Using 404 instead hides it completely, and a single service-layer flag can protect all related endpoints while the frontend hides UI via feature flags. For admin APIs though, 403 or 410 makes more sense since admins need to know the feature is just disabled.
I was checking the access logs, and my eyes caught a weird pattern. The review endpoint on a city portal project that was supposed to be completely disabled was spitting out a 403 Forbidden status.
At first, I didn't think much of it. After all, 403 means "access denied", isn't that correct? But when I thought about it more, a 403 indirectly admits that the resource actually exists there, the user just doesn't have permission. For a feature we intentionally disabled entirely, this silent admission is a small risk we don't need to take.
This is exactly why returning a 404 makes so much more sense. According to MDN documentation, server owners are allowed to send a 404 instead of a 403 if they do not wish to acknowledge the existence of a resource [6]. RFC 9110 reinforces this: a 404 status doesn't just mean "not found", it can also mean the server is unwilling to disclose that such a representation exists [7].
Why Not 200 Empty or 403?
You might wonder why we don't just return a 200 OK with an empty array. That is dangerous. A different response structure between an active and inactive feature can become a side-channel leak for an attacker fuzzing our endpoints.
And why not stick with 403? Because 403 is a subtle invitation for endpoint enumeration. If an attacker sees a 403, they know something is there and will look for another way in. If they get a 404, they will just assume a typo and move on.
Implementing a Cheap and Effective Gate
How does this work in practice? I don't check permissions on every endpoint separately. I built one cheap gate at the service layer.
Every request runs a simple boolean probe:
SELECT rating_enabled WHERE status = 'published' AND deleted_at IS NULL
If the row is missing or the value is false, the domain error is immediately mapped to an HTTP 404 NOT_FOUND in the handler. This single gate secures three endpoints at once: review creation, the list of approved reviews, and the rating distribution.
On the client side, our public DTO exposes two flags: rating_enabled and rental_enabled. This allows the frontend to hide rating-related buttons or UI right from the start, without waiting for a request to fail. Internal type IDs remain safe and unexposed to the public.
There is one exception I need to emphasize, though. If this is an authenticated admin API, the logic must be different. Admins actually need to know that the feature exists but is currently disabled. In that situation, a 403 Forbidden or 410 Gone is a much more honest and useful semantic choice for their workflow.