Skip to content

RequestsHTTPTransport does not set headers on session like AIOHTTPTransport does #613

Description

@shtrom

Describe the bug
When creating a transport = RequestsHTTPTransport with specific headers, transport.session.headers doesn't match what was given to the constructor (and is in transport.headers`)

This is at odd with the behaviour of AIOHTTPTransport, where we get transport.session.headers == transport.headers

To Reproduce
Steps to reproduce the behavior:

import pytest

from gql.transport.requests import RequestsHTTPTransport
from gql.transport.aiohttp import AIOHTTPTransport

@pytest.mark.asyncio
async def test_aio_http_transport_session():
    transport = AIOHTTPTransport("url", headers={"test": "header"})
    await transport.connect()
    assert transport.headers == transport.session.headers


def test_requests_http_transport_session():
    transport = RequestsHTTPTransport("url", headers={"test": "header"})
    transport.connect()
    assert transport.headers == transport.session.headers
$ uv run pytest -k test_gql -v                130 ↵   ✹ ✭ issue202/rest-client-headers
================================================= test session starts =================================================
platform linux -- Python 3.12.11, pytest-9.1.1, pluggy-1.6.0 -- /home/shtrom/work/simple-github/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/shtrom/work/simple-github
configfile: pyproject.toml
plugins: anyio-4.14.2, responses-0.6.0, aioresponses-0.3.0, mock-3.15.1, asyncio-1.4.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 34 items / 32 deselected / 2 selected

test/test_gql.py::test_aio_http_transport_session PASSED                                                        [ 50%]
test/test_gql.py::test_requests_http_transport_session FAILED                                                   [100%]

====================================================== FAILURES =======================================================
________________________________________ test_requests_http_transport_session _________________________________________

    def test_requests_http_transport_session():
        transport = RequestsHTTPTransport("url", headers={"test": "header"})
        transport.connect()
>       assert transport.headers == transport.session.headers
E       AssertionError: assert {'test': 'header'} == {'User-Agent'... 'keep-alive'}
E
E         Left contains 1 more item:
E         {'test': 'header'}
E         Right contains 4 more items:
E         {'Accept': '*/*',
E          'Accept-Encoding': 'gzip, deflate, br, zstd',
E          'Connection': 'keep-alive',...
E
E         ...Full output truncated (7 lines hidden), use '-vv' to show

test/test_gql.py:17: AssertionError
=============================================== short test summary info ===============================================
FAILED test/test_gql.py::test_requests_http_transport_session - AssertionError: assert {'test': 'header'} == {'User-Agent'... 'keep-alive'}
===================================== 1 failed, 1 passed, 32 deselected in 0.10s ======================================

Expected behavior
transport.session.headers == transport.headers for RequestHTTPTransport, same as AIOHTTPTransport already does.

System info (please complete the following information):

  • OS: Linux x86_64, Ubuntu 26.04
  • Python version: 3.14.4
  • gql version: 4.0
  • graphql-core version: 3.2.11

Activity

  1. leszekhanusz commented on Aug 26, 2026

    @leszekhanusz
    Collaborator

    I'm a bit torn on this issue.

    The session attribute in RequestsHTTPTransport could have the headers, cookies, auth, and verify defined directly there instead of at each POST request, but that's an implementation detail of the transport. Nowhere in the GQL contract (the documentation or the code reference) does it say that there is a session attribute that you could reuse for something else, containing all that.

    On the AIOHTTPTransport, it is indeed what is happening, those things are defined in the session and not on the POST call.

    On the HTTPXTransport, it is even easier: the headers are passed directly to the client of httpx during the connect call and is not even saved in a headers attribute, there is no session attribute, all is in one place.

    Now where it gets tricky, is that if we now update the headers dict of the RequestsHTTPTransport session during connect (like your PR #614), we could in theory remove it from the POST call like it is done on AIOHTTPTransport. Except... that it would be a breaking changes for users who were manually changing the headers attribute after the connection.

    Another thing to consider: even if we put the headers, the cookies, auth and verify in the session, the user could put other arguments for the post method of requests thanks to the **kwargs arguments passed to the transport, things like proxies, hooks, ... which are not directly supported by gql, so the session attribute would not be representative of all the connection needs of the user.

  2. shtrom commented on Aug 26, 2026

    @shtrom
    Author

    Nowhere in the GQL contract (the documentation or the code reference) does it say that there is a session attribute that you could reuse for something else, containing all that.

    Yep, we are hitting Hyrum's law there (:

    I think the only (weak) argument in support would be that transport.session is not prefixed with a _, which makes it look somewhat publicly accessible, rather than an implementation detail?

    OTOH, I agree with the rest of the arguments against.

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