Return JSON error body when /oauth/register is rate limited
What does this MR do and why?
Closes #602891 (closed).
POST /oauth/register (OAuth 2.0 Dynamic Client Registration, RFC
7591) returns a plain-text body when rate limited, while every other
non-2xx response on this controller already returns JSON. RFC 6749
section 5.2 and RFC 7591 section 3.2.2 define a JSON error-response
shape for OAuth endpoints, so standards-compliant clients fail to
parse the body and surface a confusing parse error instead of a clean
rate-limit signal — the issue documents this concretely against the
MCP TypeScript SDK, which affects Claude Code, Cursor, Duo CLI, and
mcp-remote.
Fix
Oauth::DynamicRegistrationsController#check_rate_limit used
check_rate_limit!'s default (plain-text) response. This uses the
concern's existing block form — already used elsewhere in the
codebase (e.g. SessionsController) — to render the same JSON error
shape this controller already uses for its 400 responses:
{ "error": "temporarily_unavailable", "error_description": "Rate limit exceeded, retry after 3600 seconds" }Also adds a Retry-After header (seconds), derived from
Gitlab::ApplicationRateLimiter.period_for(:oauth_dynamic_registration)
rather than a hardcoded value, so it can't drift from the actual
configured rate limit.
Screenshots or screen recordings
N/A — backend-only change.
How to set up and validate locally
bin/rspec spec/requests/oauth/dynamic_registrations_controller_spec.rbNew coverage added for the rate-limited path (JSON body, status,
Retry-After header, and that no application is created). Note the
controller currently skips rate limiting entirely in test/
development (return if Rails.env.test? || Rails.env.development?),
so the new spec stubs Rails.env.test? to false to actually
exercise the throttled branch — the same pattern already used
elsewhere in the suite (e.g. spec/lib/gitlab/tracking/destinations/snowplow_spec.rb).
Disclosure
This fix was drafted with AI assistance (Claude Code). The issue itself was reported with an unusually thorough root-cause writeup (including the exact RFCs and a suggested fix scoped to this controller); the fix and tests were AI-assisted, verified via a full local run of the controller's spec file (58 examples, 0 failures) rather than taken on faith.