Fix runner token reset returning 500 for unassigned project runners
What does this MR do and why?
When a project-type runner that is not assigned to any project attempts to rotate its authentication token via POST /api/v4/runners/reset_authentication_token or POST /api/v4/runners/:id/reset_authentication_token, an unhandled ActiveRecord::RecordInvalid exception propagates to the Grape response layer and produces an HTTP 500.
This is a client-side misconfiguration (the runner has no projects assigned), but the 500 response causes false WebserviceServicePumaErrorSLOViolation alerts on GitLab Dedicated tenants — each requiring 30-60 minutes of on-call investigation to rule out genuine service degradation.
This MR rescues ActiveRecord::RecordInvalid in Ci::Runners::ResetAuthenticationTokenService#execute! and returns a ServiceResponse.error with reason: :unprocessable_entity. Both API endpoints now check the service result and return 422 Unprocessable Entity with an actionable error message instead of letting the exception propagate as a 500.
Changes
-
app/services/ci/runners/reset_authentication_token_service.rb: RescueActiveRecord::RecordInvalidand returnServiceResponse.error(message:, reason: :unprocessable_entity) -
lib/api/ci/runner.rb: Check service result and return appropriate error status viarender_api_error!(result.message, result.reason) -
lib/api/ci/runners.rb: Same fix for thePOST /runners/:id/reset_authentication_tokenendpoint - Added specs for all three layers (service, both API endpoints)
Note: Authn::Tokens::RunnerAuthenticationToken#revoke! (used by DELETE /api/v4/admin/token) also calls this service. That caller already checks response.success? and will now correctly receive the ServiceResponse.error instead of an unhandled exception — no additional changes needed there.
References
Closes #592412 (closed)
Screenshots or screen recordings
N/A — backend-only change, no UI impact.
How to set up and validate locally
# In rails console
# 1. Create a project runner and then remove its project assignments
project = FactoryBot.create(:project)
runner = FactoryBot.create(:ci_runner, :project, projects: [project])
runner.runner_projects.delete_all
# 2. Verify runner is in invalid state
puts "Runner valid? #{runner.valid?}" # => false
puts "Runner errors: #{runner.errors.full_messages}" # => ["Runner needs to be assigned to at least one project"]
# 3. Test the service — should return error, NOT raise an exception
service = Ci::Runners::ResetAuthenticationTokenService.new(runner: runner, current_user: nil, source: :runner_api)
result = service.execute!
puts "Error? #{result.error?}" # => true
puts "Message: #{result.message}" # => "Validation failed: Runner needs to be assigned to at least one project"
puts "Reason: #{result.reason}" # => :unprocessable_entity
# 4. Verify token was not changed
puts "Token unchanged? #{runner.reload.token == runner.token}" # => true
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.