Document 401 responses to a missing permission in the REST API
What does this MR do?
The status code table on the REST API troubleshooting page says that 401 Unauthorized means "The user isn't authenticated. A valid user token is necessary." Several endpoints also return 401 Unauthorized to an authenticated user who doesn't have permission for the request, and their own pages already say so. Merge a merge request lists 401 with "This user does not have permission to accept this merge request.", Reset approvals for a merge request says "Human users receive a 401 Unauthorized response.", and List all personal access tokens returns 401 "if a non-administrator uses the user_id attribute to filter for other users". The body of those responses is {"message":"401 Unauthorized"}, the same body GitLab returns for a token that doesn't exist, so a client that relies on the table tells its user to check a token that works.
This MR keeps the text of the 401 row and adds that some endpoints also return this status code for a missing permission, with a link to a new Status code 401 section next to Status code 400. The section says when the API returns 401 because of the token, gives the three examples above with links to their pages, shows the body they share with an unknown token, and explains how to tell the two apart: retrieve the current user with the same token. If that returns 200 OK, GitLab accepts the token and the original request was refused for another reason. A 403 Forbidden also means GitLab accepts the token, which then can't read the current user, as with a fine-grained personal access token that has no permission to read the user account. If GET /user also returns 401 Unauthorized, GitLab doesn't accept the token. The section also says that this check doesn't apply to CI/CD job tokens, which can't access GET /user. The 403 Forbidden row is unchanged.
The MR documents the behavior and does not change it. The API style guide says "We must not make breaking changes to our REST API v4, even in major GitLab releases." and lists "Changing any status code other than 500" as a breaking change, so I'm not proposing to move these endpoints to 403 here. When I added cancel_auto_merge beside the deprecated cancel_merge_when_pipeline_succeeds route in !255702 (merged), the new route answered this refusal with 403 and the old one kept its 401. I list the places I found that return 401 for a missing permission below in case they are useful.
I found this while maintaining gitlab-mcp-server, an MCP server for GitLab. A refused merge request approval reached it as 401, and it reported that the token might be invalid or expired.
Evidence, file and line on master at 7f4e1ed0
lib/api/helpers.rb:617-619:unauthorized!renders401with the message401 Unauthorized.lib/api/helpers.rb:1037-1040andlib/gitlab/auth/auth_finders.rb:434: a personal access token that doesn't exist raisesUnauthorizedError, whichinitial_current_useranswers with the sameunauthorized!. That is why the two bodies are identical.- The three examples on the page:
lib/api/merge_requests.rb:905(unauthorized! unless merge_request.can_be_merged_by?(current_user)),lib/api/merge_request_approvals.rb:148(unauthorized! unless current_user.can?(:reset_merge_request_approvals, merge_request) && ...), andlib/api/helpers/personal_access_tokens_helpers.rb:80(restrict_non_admins!, whichlib/api/personal_access_tokens.rb:11calls for non-administrators). lib/api/users.rb:1356andlib/api/api_guard.rb:235-238:GET /userrequires theread_userpermission of a fine-grained personal access token, and a token without it gets403withinsufficient_granular_scope. A legacy token whose scopes don't include one thatlib/api/api.rb:58-59andlib/api/users.rb:9-12allow on this route, such as a token with onlyself_rotate, gets403withinsufficient_scope(lib/gitlab/auth/auth_finders.rb:231, rendered atlib/api/api_guard.rb:217-223).lib/gitlab/auth/auth_finders.rb:98: a CI/CD job token is only read on routes that setjob_token_allowed, andlib/api/users.rbsets it on no route. The Users API is also not in the list of endpoints a job token can access indoc/ci/jobs/ci_job_token.md.
Measured on GitLab.com on 2026-10-08 with read-only requests. My account is not a Maintainer of this project, and lib/api/remote_mirrors.rb:12 and lib/api/resource_access_tokens.rb:32 refuse a non-Maintainer with unauthorized!:
GET /api/v4/user (unknown token) 401 {"message":"401 Unauthorized"}
GET /api/v4/projects/278964/remote_mirrors (my token) 401 {"message":"401 Unauthorized"}
GET /api/v4/projects/278964/access_tokens (my token) 401 {"message":"401 Unauthorized"}
GET /api/v4/user (my token) 200None of the three 401 responses carried a WWW-Authenticate header.
Places where the REST API returns 401 for a missing permission, on master at 7f4e1ed0
Each of these answers 401 to an authenticated user who lacks a permission, a role or a license, or who as a non-administrator names a token they can't see. These are the ones I found, and the list is not exhaustive:
- Merge requests: merge (
lib/api/merge_requests.rb:905), cancel auto-merge through the deprecatedcancel_merge_when_pipeline_succeedsroute (lib/api/merge_requests.rb:957), approve (lib/api/merge_request_approvals.rb:102, on any refusal ofMergeRequests::ApprovalService, such as "User is not eligible to approve" atapp/services/merge_requests/approval_service.rb:9), and reset approvals (lib/api/merge_request_approvals.rb:148). Its replacement,cancel_auto_merge(!255702 (merged)), answers the same refusal with403(lib/api/merge_requests.rb:979). - Merge trains: add a merge request when the service answers
:forbidden(ee/lib/api/merge_trains.rb:173). - Remote mirrors: every route (
lib/api/remote_mirrors.rb:12). - Project and group access tokens: list and get without
read_resource_access_tokens(lib/api/resource_access_tokens.rb:32and:63), rotate by ID for a non-administrator who can't manage the resource's tokens or names a token that doesn't exist (:200), and self-rotate for a bot token that is not one of the resource's (lib/api/resource_access_tokens/self_rotation.rb:46). - Personal access tokens: get and rotate by ID for a non-administrator, when the token doesn't exist or they can't read it (
lib/api/personal_access_tokens.rb:71and:100), anduser_idnaming another user (lib/api/helpers/personal_access_tokens_helpers.rb:80). - Emoji reactions: delete one somebody else awarded (
lib/api/award_emoji.rb:136). - Groups: update by a user who can administer the group's runners but not the group, when the request changes anything other than
shared_runners_setting(lib/api/groups.rb:100). A user with neither permission gets403fromauthorize_any!atlib/api/groups.rb:377, in the same route. - Projects: create a fork relationship when the project's namespace is not a valid fork target for the user, with the reason
Target Namespace(lib/api/projects.rb:956). Forking a project into such a namespace answers404instead (:687). - External status checks: every route when the project lacks the licensed feature (
ee/lib/api/status_checks.rb:16), the list withoutread_external_status_check(ee/lib/api/status_checks.rb:67), and the update, whose service builds its own401(ee/app/services/external_status_checks/update_service.rb:40). The create and delete routes are the subject of !260486 (merged). - Security: security scans (
ee/lib/api/security_scans.rb:55), project security settings (ee/lib/api/project_security_settings.rb:30and:56), and group security settings (ee/lib/api/group_security_settings.rb:37). - SAML group links: all four routes (
ee/lib/api/saml_group_links.rb:44,:76,:115and:144). #383531 reports a401from one of these routes as misleading. - MLflow:
check_api_write!andcheck_api_model_registry_write!(lib/api/ml/mlflow/api_helpers.rb:15and:23). - Code Suggestions: direct access for a user who isn't allowed to use Code Suggestions (
ee/lib/api/code_suggestions.rb:296). authorize_manage_saml!inee/lib/ee/api/helpers.rb:191-193does the same, but no API route calls it.
Related issues
- #383531: a
401from the SAML group links API, reported as misleading. - #624056: agreeing a consistent
401and403error shape for the modular feature APIs.
Author's checklist
- Optional. Consider taking the GitLab Technical Writing Fundamentals course.
- Follow the:
- If you're adding a new page, add the product availability details under the H1 topic title.
- If you are a GitLab team member, request a review based on:
- The documentation page's metadata.
- The associated Technical Writer.
If you are a GitLab team member and only adding documentation, do not add any of the following labels:
~"frontend"~"backend"~"type::bug"~"database"
These labels cause the MR to be added to code verification QA issues.
Reviewer's checklist
Documentation-related MRs should be reviewed by a Technical Writer for a non-blocking review, based on Documentation Guidelines and the Style Guide.
If you aren't sure which tech writer to ask, use roulette or ask in the #docs Slack channel.
- If the content requires it, ensure the information is reviewed by a subject matter expert.
- Technical writer review items:
- Ensure docs metadata is present and up-to-date.
- Ensure the appropriate labels are added to this MR.
- Ensure a release milestone is set.
- If relevant to this MR, ensure content topic type principles are in use, including:
- The headings should be something you'd do a Google search for. Instead of
Default behavior, say something likeDefault behavior when you close an issue. - The headings (other than the page title) should be active. Instead of
Configuring GDK, say something likeConfigure GDK. - Any task steps should be written as a numbered list.
- If the content still needs to be edited for topic types, you can create a follow-up issue with the docs-technical-debt label.
- The headings should be something you'd do a Google search for. Instead of
- Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review.