Skip to content

fix(client): use HTTP reason for bodyless error responses - #3467

Open
HiyuP157 wants to merge 2 commits into
python-gitlab:mainfrom
HiyuP157:fix/3461-head-error-message
Open

HiyuP157 wants to merge 2 commits into
python-gitlab:mainfrom
HiyuP157:fix/3461-head-error-message

Conversation

@HiyuP157

@HiyuP157 HiyuP157 commented Nov 8, 2026 •

Copy link
Copy Markdown

Changes

Fixes #3461

Problem

A failed HEAD request has no response body by HTTP semantics. For example:

try:
    project.files.head("nonexistent.yaml", ref="main")
except gitlab.exceptions.GitlabHeadError as exc:
    print(exc.response_code)  # 404
    print(repr(exc.error_message))  # ''
    print(str(exc))  # '404: '

This makes logs and CLI output show an empty error message even though the HTTP status has a standard reason phrase.

Root cause

Gitlab.http_request() initializes error_message from result.content and then tries to replace it with the JSON message/error field. For a bodyless HEAD error, result.content is b"" and result.json() raises ValueError, so both attempts leave the message empty. GitlabError then decodes that empty value and GitlabError.__str__() renders "<status>: ".

The specialized GitlabHeadError raised by ProjectFileManager.head() receives the same empty message through on_http_error.

Solution

When no error message was parsed from the response, fall back to:

  1. the response's HTTP reason phrase, when the server provided one; or
  2. the standard phrase for the status code from http.HTTPStatus.

The change is limited to error-message construction:

  • response_code is unchanged;
  • response_body remains exactly what the server returned (including b"" for HEAD);
  • existing JSON message/error values still take precedence;
  • request sending, retries, and redirect handling are unchanged.

Validation

Added regression coverage for:

  • a bodyless HEAD error at the Gitlab.http_request() / http_head() level;
  • the reported project.files.head() path, including GitlabHeadError and str(exc);
  • a JSON error body still taking precedence over the HTTP reason phrase;
  • an unknown status code still raising the original GitlabHttpError instead of a fallback lookup error.

Local verification on Python 3.13:

  • pytest -q tests/unit/test_gitlab_http_methods.py tests/unit/objects/test_repositories.py — 65 passed
  • pytest -q tests/unit — 726 passed, 3 skipped
  • tox -e black -- --check — passed
  • tox -e isort -- --check-only --diff — passed
  • tox -e flake8 — passed
  • tox -e mypy — passed
  • tox -e pylint — passed (10.00/10)
  • cz check --rev-range HEAD~1..HEAD — passed

A standalone reproduction of the issue now reports error_message='Not Found' and str(exc)='404: Not Found', with response_body=b''.

Functional tests were not run because they require a Dockerized GitLab instance; the regression is covered by mocked HTTP unit tests matching the issue's reproduction.

Impact

Small, backward-compatible error-reporting improvement. The only visible behavior change is that empty error messages for bodyless failed responses now include an HTTP reason phrase instead of trailing whitespace.

Documentation and testing

  • Documentation update is not required for this internal error-reporting fix
  • Unit tests added for the low-level HTTP path, the reported project.files.head() path, and JSON-message precedence

Failed HEAD responses normally have no body, so Gitlab.http_request()
left error_message empty and raised errors rendered as "404: ".

Fall back to the response reason phrase, or the standard HTTPStatus
phrase when the server did not provide one, only when no message could
be parsed from the response. JSON response messages still take
precedence, and response_body remains unchanged.

Add regression coverage for the low-level HEAD path,
ProjectFileManager.head(), and JSON message precedence.

Fixes python-gitlab#3461
@codecov

codecov Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.79%. Comparing base (bb9ea1b) to head (d0cb40c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3467   +/-   ##
=======================================
  Coverage   95.78%   95.79%           
=======================================
  Files         100      100           
  Lines        6172     6179    +7     
=======================================
+ Hits         5912     5919    +7     
  Misses        260      260           
Flag Coverage Δ
api_func_v4 83.79% <37.50%> (-0.07%) ⬇️
cli_func_v4 78.73% <37.50%> (-0.06%) ⬇️
unit 90.33% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
gitlab/client.py 98.57% <100.00%> (+0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Exercise the defensive ValueError path used when the server returns a
status code that is not part of http.HTTPStatus, so the fallback does
not mask the original GitlabHttpError.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bodyless HEAD error responses produce an empty error_message (str(e) == "404: ")

1 participant