Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting cookies in AIOHttpTransport doesn't work #197

Closed
snowPu opened this issue Mar 2, 2021 · 11 comments
Closed

Setting cookies in AIOHttpTransport doesn't work #197

snowPu opened this issue Mar 2, 2021 · 11 comments
Labels
type: documentation An issue or pull request for improving or updating the documentation type: question or discussion Issue discussing or asking a question about gql

Comments

@snowPu
Copy link

snowPu commented Mar 2, 2021

I'm trying to set cookies in the transport to execute a query.
This works when I use RequestsHTTPTransport but not when I use AIOHTTPTransport.
The cookie needs to be present in the POST request. In case it's not present, the client gets redirected to the login page.
So if I'm using requests library directly, it should look like this:

cookies = authenticate(url, username, password)

json = { 'query' : '{ queryName { id }}' }

r = requests.post(url, json=json, cookies=cookies)

This works:

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

cookies=authenticate(url, username, password).get_dict()
transport = RequestsHTTPTransport(url=url, timeout=900, cookies=cookies)

with Client(
    transport=transport,
#     fetch_schema_from_transport=True,
    execute_timeout=1200,
) as client:
    query = gql('''
        query {
            queryName { id } 
        }
    ''')
    result = client.execute(query)
    print(result)

This redirects me to the login page.

from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport

cookies=authenticate(url, username, password).get_dict()
transport = AIOHTTPTransport(url=url, timeout=900, cookies=cookies)

async with Client(
    transport=transport,
#     fetch_schema_from_transport=True,
    execute_timeout=1200,
) as client:
    query = gql('''
        query {
            queryName { id } 
        }
    ''')
    result = await client.execute(query)
    print(result)

I need to send the same cookies for uploading a file via graphQL which is why I need to use AIOHttpTransport. How do I send cookies the same way as in requests?

Note: Tried sending via extra_args but that didn't work.

@leszekhanusz
Copy link
Collaborator

What does the authenticate method returns? What is the type of cookies?

I made two tests to test the cookies for RequestsHTTPTransport and the AIOHTTPTransport and both seems to works the same way with a simple dict:

@pytest.mark.asyncio                                                              
async def test_aiohttp_cookies(event_loop, aiohttp_server):
    from aiohttp import web                                 
    from gql.transport.aiohttp import AIOHTTPTransport                                 
                               
    async def handler(request):                                                        
        assert "COOKIE" in request.headers                
        assert "cookie1=val1" == request.headers["COOKIE"]
                                                                                       
        return web.Response(text=query1_server_answer, content_type="application/json")

    app = web.Application()   
    app.router.add_route("POST", "/", handler)
    server = await aiohttp_server(app)
                                                                                      
    url = server.make_url("/")
                                                            
    sample_transport = AIOHTTPTransport(url=url, cookies={"cookie1": "val1"})            
                                   
    async with Client(transport=sample_transport,) as session:
                                         
        query = gql(query1_str)            

        # Execute query asynchronously       
        result = await session.execute(query)
                                  
        continents = result["continents"]
                                         
        africa = continents[0]
                                                      
        assert africa["code"] == "AF"

@pytest.mark.aiohttp                                       
@pytest.mark.asyncio       
async def test_requests_cookies(event_loop, aiohttp_server, run_sync_test):
    from aiohttp import web
    from gql.transport.requests import RequestsHTTPTransport
                                                                                  
    async def handler(request):                            
        assert "COOKIE" in request.headers                  
        assert "cookie1=val1" == request.headers["COOKIE"]                             
                               
        return web.Response(text=query1_server_answer, content_type="application/json")
                                                          
    app = web.Application()                               
    app.router.add_route("POST", "/", handler)                                         
    server = await aiohttp_server(app)                                                 

    url = server.make_url("/")
                                              
    def test_code():                  
        sample_transport = RequestsHTTPTransport(url=url, cookies={"cookie1": "val1"})
                              
        with Client(transport=sample_transport,) as session:
                                                                                         
            query = gql(query1_str)
                                                              
            # Execute query synchronously
            result = session.execute(query)

            continents = result["continents"]
                                             
            africa = continents[0]
                                         
            assert africa["code"] == "AF"
                              
    await run_sync_test(event_loop, server, test_code)

But the behavior may change if the type of the cookies param is not a dict.
For example, aiohttp supports the following types:

LooseCookiesIterables = Iterable[
    Tuple[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]]
]
LooseCookies = Union[
    LooseCookiesMappings,
    LooseCookiesIterables,
    "BaseCookie[str]",
]

@leszekhanusz leszekhanusz added the status: needs more information Waiting on the issue author to add additional information label Apr 6, 2021
@Izayda
Copy link

Izayda commented Apr 15, 2021

I can also confirm this behavior.

gql version 3.0.0a5

I have method, that return me cookie dict.

RequestsHTTPTransport (works fine)

import requests
from gql import Client, gql
from gql.transport.requests import RequestsHTTPTransport


class SomeApi:
    def __init__(self, graphql_url="https://devroooms.ru/admin-api"):
        self.graphql_url = graphql_url
        self.transport = RequestsHTTPTransport(url=graphql_url)
        self.client = Client(transport=self.transport, fetch_schema_from_transport=True)
        self.cookies = self.cookie_login()
        self.transport.cookies = self.cookies

    def cookie_login(self):
        query = """
        mutation AttemptLogin(
          $username: String!
          $password: String!
          $rememberMe: Boolean!
        ) {
          login(username: $username, password: $password, rememberMe: $rememberMe) {
            ...CurrentUser
            ...ErrorResult
            __typename
          }
        }fragment CurrentUser on CurrentUser {
          id
          identifier
          channels {
            id
            code
            token
            permissions
            __typename
          }
          __typename
        }
        fragment ErrorResult on ErrorResult {
          errorCode
          message
          __typename
        }
        """

        variable_values = {'username': '[email protected]', 'password': '[email protected]', 'rememberMe': True}

        s = requests.session()
        p = s.post(self.graphql_url, json={"query": query, "variables": variable_values})

        if p.status_code == 200:
            return s.cookies.get_dict()
        else:
            raise Exception

        return None

    def get_all_facets(self):
        query = gql("""
                query Query {
                  facets {
                    items {
                      isPrivate
                      id
                      createdAt
                      updatedAt
                      languageCode
                      name
                      code
                      values {
                        id
                        createdAt
                        updatedAt
                        languageCode
                        name
                        code
                        customFields
                      }
                      customFields
                    }
                    totalItems
                  }
                }
                """)

        result = self.client.execute(query, variable_values={})

        if result:
            if result.get('facets'):
                if result['facets'].get('items'):
                    return result['facets']['items']

        return None


if __name__ == "__main__":
    api = SomeApi()
    api.get_all_facets()

AIOHTTPTransport

If i change RequestsHTTPTransport with AIOHTTPTransport, i get:
gql.transport.exceptions.TransportQueryError: {'message': 'You are not currently authorized to perform this action', 'locations': [{'line': 2, 'column': 3}], 'path': ['facets'], 'extensions': {'code': 'FORBIDDEN'}}

@leszekhanusz
Copy link
Collaborator

Cookies are working fine on AIOHTTPTransport as can be seen in the tests above.

I suspect there are some other shenanigans here with maybe a redirect from the backend which works with requests but not with aiohttp.

Could you reproduce your problem in a test which fails using aiohttp and succeeds with requests ? Or at least provide a backend which can be tested publicly ?

@Izayda
Copy link

Izayda commented Apr 15, 2021

And tried plain aiohttp, works fine

import asyncio

import aiohttp
import json

async def main():
    graphql_url = "https://devroooms.ru/admin-api"
    session = aiohttp.ClientSession()

    variable_values = {'username': '[email protected]', 'password': '[email protected]', 'rememberMe': True}
    query = """
            mutation AttemptLogin(
              $username: String!
              $password: String!
              $rememberMe: Boolean!
            ) {
              login(username: $username, password: $password, rememberMe: $rememberMe) {
                ...CurrentUser
                ...ErrorResult
                __typename
              }
            }fragment CurrentUser on CurrentUser {
              id
              identifier
              channels {
                id
                code
                token
                permissions
                __typename
              }
              __typename
            }
            fragment ErrorResult on ErrorResult {
              errorCode
              message
              __typename
            }
            """
    async with session.post(graphql_url, data={"query": query, "variables": json.dumps(variable_values)}) as resp:
        print(await resp.text())

    query = """
                    query Query {
                      facets {
                        items {
                          isPrivate
                          id
                          createdAt
                          updatedAt
                          languageCode
                          name
                          code
                          values {
                            id
                            createdAt
                            updatedAt
                            languageCode
                            name
                            code
                            customFields
                          }
                          customFields
                        }
                        totalItems
                      }
                    }
                    """
    async with session.post(graphql_url, data={"query": query}) as resp:
        print(await resp.text())


    await session.close()


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

@leszekhanusz
Copy link
Collaborator

I don't see any cookies in your previous message ??

@Izayda
Copy link

Izayda commented Apr 15, 2021

Updated my comments with live example with login and pass to test.

I don't see any cookies in your previous message ??

Yes, because aiohttp.ClientSession stores cookies from first response to second request.

@leszekhanusz
Copy link
Collaborator

I understand your problem.
You can keep cookies between session using a cookieJar.

This should work:

import asyncio
import aiohttp
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport


class SomeApi:
    def __init__(self, graphql_url="https://devroooms.ru/admin-api"):
        self.graphql_url = graphql_url
        jar = aiohttp.CookieJar()
        self.transport = AIOHTTPTransport(url=graphql_url, client_session_args={'cookie_jar': jar})
        self.client = Client(transport=self.transport, fetch_schema_from_transport=True)

    async def cookie_login(self):
        query = """
        mutation AttemptLogin(
          $username: String!
          $password: String!
          $rememberMe: Boolean!
        ) {
          login(username: $username, password: $password, rememberMe: $rememberMe) {
            ...CurrentUser
            ...ErrorResult
            __typename
          }
        }fragment CurrentUser on CurrentUser {
          id
          identifier
          channels {
            id
            code
            token
            permissions
            __typename
          }
          __typename
        }
        fragment ErrorResult on ErrorResult {
          errorCode
          message
          __typename
        }
        """

        variable_values = {'username': '[email protected]', 'password': '[email protected]', 'rememberMe': True}

        async with self.client as session:
            answer = await session.execute(gql(query), variable_values=variable_values)

    async def get_all_facets(self):
        query = gql("""
                query Query {
                  facets {
                    items {
                      isPrivate
                      id
                      createdAt
                      updatedAt
                      languageCode
                      name
                      code
                      values {
                        id
                        createdAt
                        updatedAt
                        languageCode
                        name
                        code
                        customFields
                      }
                      customFields
                    }
                    totalItems
                  }
                }
                """)

        async with self.client as session:
            result = await session.execute(query, variable_values={})

        if result:
            if result.get('facets'):
                if result['facets'].get('items'):
                    return result['facets']['items']

        return None

async def main():
    api = SomeApi()

    await api.cookie_login()

    print ("login done")

    answer = await api.get_all_facets()
    print (f"result = {answer}")

asyncio.run(main())

@Izayda
Copy link

Izayda commented Apr 15, 2021

Ok, i can reproduce this error in clean aiohttp

import asyncio
import json

import aiohttp


async def main():
    graphql_url = "https://devroooms.ru/admin-api"
    session = aiohttp.ClientSession()

    variable_values = {'username': '[email protected]', 'password': '[email protected]', 'rememberMe': True}
    query = """
            mutation AttemptLogin(
              $username: String!
              $password: String!
              $rememberMe: Boolean!
            ) {
              login(username: $username, password: $password, rememberMe: $rememberMe) {
                ...CurrentUser
                ...ErrorResult
                __typename
              }
            }fragment CurrentUser on CurrentUser {
              id
              identifier
              channels {
                id
                code
                token
                permissions
                __typename
              }
              __typename
            }
            fragment ErrorResult on ErrorResult {
              errorCode
              message
              __typename
            }
            """

    cookies = None
    async with session.post(graphql_url, data={"query": query, "variables": json.dumps(variable_values)}) as resp:
        await resp.json()
        cookies = {key: value.value for key,value in resp.cookies.items()}

    # print(cookies)

    query = """
                    query Query {
                      facets {
                        items {
                          isPrivate
                          id
                          createdAt
                          updatedAt
                          languageCode
                          name
                          code
                          values {
                            id
                            createdAt
                            updatedAt
                            languageCode
                            name
                            code
                            customFields
                          }
                          customFields
                        }
                        totalItems
                      }
                    }
                    """
    async with session.post(graphql_url, data={"query": query},cookies=cookies) as resp:
        print(await resp.json())

    await session.close()


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

@leszekhanusz
Copy link
Collaborator

@Izayda did you see my comment about setting a cookie jar ?

@Izayda
Copy link

Izayda commented Apr 15, 2021

Sorry, posted previous message without page reload.

Thank you very much, you save my day,, thats works perfect!

As i can see, there are some unexpected features in aiohttp with SimpleCookie, because sometimes cookies are enclosed with quotes e.t.c.
There are some issues in aiohttp repo aio-libs/aiohttp#5397
So, i guess, authors problem is also caused by using cookies without CookieJar.

@leszekhanusz
Copy link
Collaborator

I added some documentation about this in PR #202

@leszekhanusz leszekhanusz added type: documentation An issue or pull request for improving or updating the documentation type: question or discussion Issue discussing or asking a question about gql and removed status: needs more information Waiting on the issue author to add additional information labels Apr 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation An issue or pull request for improving or updating the documentation type: question or discussion Issue discussing or asking a question about gql
Projects
None yet
Development

No branches or pull requests

3 participants