When the Guard Cannot Answer: Fail-Closed Media Deletion
A failing usage census used to count as permission to delete. Now ErrCensusUnavailable cancels the delete: row and file survive, client gets 503.
TL;DR
Media deletion used to fail open: if the usage-count query errored, the code assumed the file was safe to delete. The fix returns ErrCensusUnavailable, mapped to a 503 CENSUS_UNAVAILABLE response, with real details logged server-side per OWASP guidance. Tests now verify deletion is blocked, data stays intact, and no stray files are written.
When an administrator clicks the delete button on a media item in the KotaPortal project, the database census query is experiencing a failure. Instead of halting the process, the old code treated this lack of an answer as unused. The deletion process then proceeded under an unknown status.
The Locked Fail-Open Policy
This behavior was not an accident that escaped testing. An old characterization test named TestDeleteProceedsWhenUsageCountFails explicitly locked in this fail-open policy, citing pending owner decisions. This test inadvertently legitimized the assumption that an error in the CountUsages query meant the file was safe to delete.
The old policy relied on a single condition: err == nil && n > 0. The logic reads reasonably on screen, but it conflates two different states. A zero value means the census truly answered that there is no usage. An error means the question was never answered, and the dangerous delusion is treating a missing answer as an answer stating it is safe.
Fail-Closed Mechanism and 503 Mapping
The fix toward a fail-closed principle begins in the Delete function of the service file. The old condition that ignored errors is replaced by returning ErrCensusUnavailable. On the handler side, this error is mapped to an HTTP 503 status with the code CENSUS_UNAVAILABLE. The message to the client remains generic — the actual error details are logged safely via slog.Error.
The test that previously locked in the dangerous behavior was rewritten as TestDeleteBlockedWhenUsageCountFails. This new test verifies that the database row and supporting files remain intact. It also ensures no stray files are written to the upload directory when the deletion is canceled.
Server-side details must not simply vanish. Every failed census is now logged via slog.Error with the media_id, media URL, and the original error. Without this trace, a fail-closed policy becomes a silencer: the admin only sees a 503 without knowing which infrastructure component is ailing. This separation follows the same error handling doctrine: generic responses for the client, details for investigation [2].
Security Foundations and Protocols
The fundamental principle is strict: when the guard cannot answer, the system must not assume it as permission. Returning a 503 Service Unavailable status is an honest representation of the system condition. According to the RFC 9110 specification, a 503 status indicates that the server is currently unable to handle the request due to temporary overloading, and the request may be retried later [1].
Using a 500 status would send the wrong signal to the client, implying that the deletion process itself is broken. This approach aligns with the OWASP Error Handling guidelines. Unhandled errors can provide additional information to an attacker. For unexpected errors, the system must return a generic response to the client, while error details are logged server-side for further investigation [2].
Differentiating between 503 and 500 also preserves operational decision-making. A 500 implies a broken deletion, whereas the data is intact and only the checker failed. RFC 9110 defines 503 as a temporary condition likely to recover after a delay, and the server may attach a Retry-After header [1]. The handler message closes this loop: deletion is canceled so that files still in use are not deleted; please try again in a few moments.
The testing side closes the possibility of regression from two directions. The service test uploads a real file first, runs the delete with a consistently failing census, and then ensures the row and file still exist. The handler test adds one more claim: no stray files are written to the upload directory during the canceled delete. A canceled operation must be truly canceled, not halfway through leaving artifacts. The OpenAPI documentation for the media delete endpoint follows in a separate commit, ensuring the 503 CENSUS_UNAVAILABLE contract is recorded for API consumers, not just in the code.