Skip to content

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

Open

Description

@hidingmyname

Description of the problem, including code/CLI snippet

Location (python-gitlab/python-gitlab @ default branch 8daa940):

            error_message = result.content
            try:
                error_json = result.json()
                for k in ("message", "error"):
                    if k in error_json:
                        error_message = error_json[k]
            except (KeyError, ValueError, TypeError):
                pass
            ...
            raise gitlab.exceptions.GitlabHttpError(
                response_code=result.status_code,
                error_message=error_message,
                response_body=result.content,
            )

Problem

HEAD responses have no body (normal HTTP semantics), so when a HEAD request fails, e.g. project.files.head() for a path that does not exist, the raised exception has an empty error_message and its string form is just the status code followed by an empty message. In Gitlab.http_request() the message is taken from the raw response content and, if parseable, the JSON message/error field. For a bodyless response both are empty and there is no fallback, so log lines and CLI output only show 404: .

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

Reproduction

Minimal unit-level reproduction with responses:

import pytest, responses, gitlab

@responses.activate
def test_head_404_message():
    gl = gitlab.Gitlab("http://localhost", private_token="t", api_version="4")
    url = "http://localhost/api/v4/projects/1/repository/files/nonexistent.yaml"
    responses.add(responses.HEAD, url, status=404)
    with pytest.raises(gitlab.GitlabHttpError) as exc:
        gl.http_request("head", "/projects/1/repository/files/nonexistent.yaml")
    assert exc.value.error_message != ""   # fails

Failing output:

E   AssertionError: assert '' != ''
E    +  where '' = GitlabHttpError(b'').error_message

Environment:

  • Commit: observed on 51eb17e6e67a; same code on current main 8daa94041261 (8.6.0)
  • Toolchain: Python 3.14.10

Expected Behavior

When the error body is empty (or yields no message), fall back to the HTTP reason phrase or the standard status text (e.g. Not Found), so str(e) reads like 404: Not Found.

Scope of the suggestion:

  • only applies when there is no body/parsed message; responses with a body are unchanged;
  • response_code and response_body stay exactly as received (response_body remains b'');
  • no attempt to read a body that HEAD does not have; the exact wording is up to the maintainers.

Actual Behavior

error_message is empty and str(e) is 404: (see the output above).

Already fixed in xanzy/go-gitlab

PR: xanzy/go-gitlab#1919 — "Ignore response body during HEAD request error handling" (merged 2024-05-14T20:35:35Z, merge commit f44d73de404). For the same bodyless HEAD case it stops trying to parse an empty body, omits the empty message from the error string, and returns ErrNotFound (404 Not Found) for 404s; only 404 gets a text there, so it is narrower than the fallback suggested here. go-gitlab is a separate client for the same GitLab REST API, so this is precedent, not an obligation.

How go-gitlab fixed it:

--- a/gitlab.go
+++ b/gitlab.go
@@ -65,6 +66,8 @@ const (
 	PrivateToken
 )
 
+var ErrNotFound = errors.New("404 Not Found")
@@ -953,20 +956,28 @@ type ErrorResponse struct {
...
 func (e *ErrorResponse) Error() string {
 	path, _ := url.QueryUnescape(e.Response.Request.URL.Path)
-	u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path)
-	return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message)
+	url := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path)
+
+	if e.Message == "" {
+		return fmt.Sprintf("%s %s: %d", e.Response.Request.Method, url, e.Response.StatusCode)
+	} else {
+		return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, url, e.Response.StatusCode, e.Message)
+	}
 }
 
 // CheckResponse checks the API response for errors, and returns them if present.
 func CheckResponse(r *http.Response) error {
 	switch r.StatusCode {
 	case 200, 201, 202, 204, 304:
 		return nil
+	case 404:
+		return ErrNotFound
 	}
 
 	errorResponse := &ErrorResponse{Response: r}
+
 	data, err := io.ReadAll(r.Body)
-	if err == nil && data != nil {
+	if err == nil && strings.TrimSpace(string(data)) != "" {
 		errorResponse.Body = data
 
 		var raw interface{}

Suggested fix

In http_request(), when the error response yields no message, fall back to the HTTP reason phrase (or the standard status text if the server sent none) before raising; response_code and response_body stay as received:

diff --git a/gitlab/client.py b/gitlab/client.py
--- a/gitlab/client.py
+++ b/gitlab/client.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import http.client
 import os
 import re
 from typing import Any, BinaryIO, cast, TYPE_CHECKING
@@ -765,6 +766,13 @@ class Gitlab:
             except (KeyError, ValueError, TypeError):
                 pass
 
+            if not error_message:
+                # Bodyless responses (e.g. to HEAD requests) carry no message,
+                # so fall back to the HTTP reason phrase.
+                error_message = result.reason or http.client.responses.get(
+                    result.status_code, ""
+                )
+
             if result.status_code == 401:
                 raise gitlab.exceptions.GitlabAuthenticationError(
                     response_code=result.status_code,

Verified on 51eb17e (Python 3.14.10, pytest 9.1.1): the test above fails without this patch and passes with it (the error then reads 404: Not Found, with response_body == b''); no other test results change.

I can submit a PR for this.

Checked: #3004 / #3006 made project.files.head() raise GitlabHeadError; they do not address the empty message. No other matching reports found (HEAD error_message empty, GitlabHeadError, "404: ").

Specifications

  • python-gitlab version: observed on 51eb17e6e67a; same code on current main 8daa94041261 (8.6.0)
  • Gitlab server version (or gitlab.com): n/a (mocked HTTP response); Python 3.14.10

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions