requests: Breaking change in 2.28.0 when using string enums as Headers (working in v2.27.1)

When using string enums as key values for the headers dict parameter with requests.get() function in v2.28.0 it raises an InvalidHeader error but using previous version v2.27.1 works well without any error. It seems there was an unexpected breaking change with that release.

Expected Result

Dict keys for the headers parameter when using requests.get() are still valid if a string enum is used.

Actual Result

Instead it raises an InvalidHeader error stating that:

Header must be of type str or bytes, not enum.

Traceback (most recent call last):
  File "/home/yair/PycharmProjects/tests/requests_error.py", line 9, in <module>
    requests.get("http://URL", headers={CustomEnum.TRACE_ID: "90e85293-afd1-4b48-adf0-fa6daf02359e"})
  File "/home/yair/.local/lib/python3.8/site-packages/requests/api.py", line 73, in get
    return request("get", url, params=params, **kwargs)
  File "/home/yair/.local/lib/python3.8/site-packages/requests/api.py", line 59, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/yair/.local/lib/python3.8/site-packages/requests/sessions.py", line 573, in request
    prep = self.prepare_request(req)
  File "/home/yair/.local/lib/python3.8/site-packages/requests/sessions.py", line 484, in prepare_request
    p.prepare(
  File "/home/yair/.local/lib/python3.8/site-packages/requests/models.py", line 369, in prepare
    self.prepare_headers(headers)
  File "/home/yair/.local/lib/python3.8/site-packages/requests/models.py", line 491, in prepare_headers
    check_header_validity(header)
  File "/home/yair/.local/lib/python3.8/site-packages/requests/utils.py", line 1037, in check_header_validity
    raise InvalidHeader(
requests.exceptions.InvalidHeader: Header part (<CustomEnum.TRACE_ID: 'X-B3-TraceId'>) from {<CustomEnum.TRACE_ID: 'X-B3-TraceId'>: '90e85293-afd1-4b48-adf0-fa6daf02359e'} must be of type str or bytes, not <enum 'CustomEnum'>

Reproduction Steps

from enum import Enum
import requests


class CustomEnum(str, Enum):
    TRACE_ID = "X-B3-TraceId"


requests.get("http://URL", headers={CustomEnum.TRACE_ID: "90e85293-afd1-4b48-adf0-fa6daf02359e"})

System Information

$ python -m requests.help
{
  "chardet": {
    "version": "3.0.4"
  },
  "charset_normalizer": {
    "version": "2.0.4"
  },
  "cryptography": {
    "version": "2.8"
  },
  "idna": {
    "version": "3.2"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.8.10"
  },
  "platform": {
    "release": "5.13.0-48-generic",
    "system": "Linux"
  },
  "pyOpenSSL": {
    "openssl_version": "1010106f",
    "version": "19.0.0"
  },
  "requests": {
    "version": "2.28.0"
  },
  "system_ssl": {
    "version": "1010106f"
  },
  "urllib3": {
    "version": "1.26.6"
  },
  "using_charset_normalizer": false,
  "using_pyopenssl": true
}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 6
  • Comments: 18 (5 by maintainers)

Most upvoted comments

We encountered this same issue when passing header values whose type is a subclass of str. Might it make sense to allow subclasses to pass validation? In this case the requests library would not be doing any conversion for the user.

Quick ping here, looking over the proposed patch I don’t see a good reason not to apply it, especially given that this kind of behavior actually violates the Python principle of ‘duck typing’. It’s unclear why Requests needs to be deeply coupled beyond isinstance. What’s the use-case where limiting to the precise coordinate of str or bytes is truly necessary? And again, we aren’t talking about string conversion, but simply if it says it’s an instance of str or bytes, letting users be responsible for what happens if it isn’t true seems reasonable, for the same reason single-underscore private methods aren’t truly private in python, but a convention.

I wrote up a patch for this on Friday. I’ve been waiting to see how widely this approach is used, because we’ve only had 4 reports with ~75% of downloads (50 million/week) now using 2.28.0. I think this falls into a very niche usage, but I agree bytes/str subclasses did work for both name and value prior to this release.

We’re going to monitor for any other issues in 2.28.0 to determine if we need a patch release. I’d be willing to merge the fix above into that with a heavy caveat that this isn’t strictly supported. Any breakages due to subclasses deviating in behavior from the base bytes/str type likely won’t be fixed.

Long term, I think we’re going to continue taking a much firmer stance on inputs because people do crazy things when the values aren’t bytes/str and expect them to just work. This becomes much more problematic when the requests are being sent to systems corrupted due to incorrect “stringification”. We can’t reasonably handle every case, so we need to draw a line somewhere on what the APIs are intended to support to provide stability.

Using enums for header names is a common (and generally regarded as a good) practice. +1.

In the ContentType enum given above, you have what looks like a string, acts like a string, and quacks like a string. It should be treated as a string. requests shouldn’t be responsible for being fed misbehaving subclasses of builtin types IMO.

Anyway, just trying to add one to the ‘user engagement’ since that was mentioned as a consideration in the 2.28.1 release.

Just want to add support for this issue by saying we are also having this issue. Our use is basically:

class ContentType(str, Enum):
    JSON = "application/json"
    XML = "text/xml"

...
def execute_request(...):
...
    headers = {"Content-Type": ContentType.JSON}
    response = requests.request(
                url=url,
                method=method,
                params=params,
                headers=headers,
                data=data
            )

Thanks for the report, @bonastreyair! So I think this is on the edge of support. Headers have always been defined to be a str or bytes and we’ve been programmatically enforcing that for header values for 6 years. While this worked in 2.27.1, it wasn’t an intentional function of the headers param. I’d like to see how widespread this issue is before we make a decision on cases like this.

In the meantime, Requests has no special logic for Enums, so casting it to a string prior to calling Requests will not affect behavior. That would be the recommendation while we gauge impact. Otherwise, 2.27.1 will continue to work as it did originally.

you can resolve this issue by converting to a string before parsing it as a key to the headers directory ` from enum import Enum class CustomEnum(Enum): TRACE_ID = “X-B3-TraceId”

headers = {CustomEnum.TRACE_ID: “90e85293-afd1-4b48-adf0-fa6daf02359e”} requests.get(“http://url/”, headers=headers)

Using enums for header names is a common (and generally regarded as a good) practice. +1.

I don’t believe anything in this change precludes the use of enums in your code. Requests is being explicit of what we accept. enum.name becomes enum.name.value and everything works fine.

I know it is easily solvable by making use of the CustomEnum.TRACE_ID.value to use the stored string instead of using the enum directly with CustomEnum.TRACE_ID… but it worked before with v2.27.1, so it feels like an unexpected breaking change… 👀