Skip to content

Clearing authorization at the request level. #1004

Closed

Description

@ScottSturdivant

Checklist

  • The bug is reproducible against the latest release and/or master.
  • There are no similar issues or pull requests to fix it yet.

Describe the bug

Merging of request-level auth unset does not take priority over Client level value.

To reproduce

Try to 'unset' the client set authorization values.

with httpx.Client(auth=('tom', 'mot123')) as client:
    r = client.get('http://httpbin.org', auth=None)
    
assert 'Authorization' not in r.request.headers['Authorization']

Expected behavior

The documentation for Merging of configuration states For all other parameters, the request-level value takes priority. and shows this example:

with httpx.Client(auth=('tom', 'mot123')) as client:
    r = client.get('https://example.com', auth=('alice', 'ecila123'))
_, _, auth = r.request.headers['Authorization'].partition(' ')

I would expect that by setting auth=None in the request level that the individual request would be sent without authorization.

Actual behavior

In reality, the authorization is not overridden. This is due to this line: https://github.com/encode/httpx/blob/master/httpx/_client.py#L258

Overriding the auth value via this method works:

with httpx.Client(auth=('tom', 'mot123')) as client:
    r = client.get('http://httpbin.org', auth=Auth())
    
assert 'Authorization' not in r.request.headers['Authorization']

Personally, I found passing in auth=None to be more intuitive than auth=Auth() to indicate that no authorization should be used.

Activity

  1. lovelydinosaur commented on May 29, 2020

    @lovelydinosaur
    Member

    Right, so...

    Presumably you'd get the same behaviour with param, cookies, headers, or auth...

    All of these default to None in the method signature, and python doesn't give us any way of determining when a user has passed an explicit auth=None vs. nothing being passed.

            params: QueryParamTypes = None,
            headers: HeaderTypes = None,
            cookies: CookieTypes = None,
            auth: AuthTypes = None,

    We could change the method signature to use open-ended keyword arguments, eg **kwargs, but that's far less descriptive, and then needs a bunch of extra checking.

    Alternately we could change them to some sentinel empty value, so eg params = EMPTY, but that's a big awkward.

    I'd be interested to know if you get similar behaviour in, say, requests, or if they're working around this being a bit of an awkward pattern in a graceful way?

    We may well just want to make sure we're documenting this well, since it's a usability gotcha, even if it's not buggy.

  2. yeraydiazdiaz commented on May 29, 2020

    @yeraydiazdiaz
    Contributor

    Alternately we could change them to some sentinel empty value, so eg params = EMPTY, but that's a big awkward.

    I was thinking on init we could set self.auth to the UNSET type if auth=None. Then at request time we can safely use None to signify "no auth"?

  3. lovelydinosaur commented on May 29, 2020

    @lovelydinosaur
    Member

    Then at request time we can safely use None to signify "no auth"?

    Only if we changed the .request() signature. Otherwise there's no way of determining the difference between the auth=None default, and an explicit auth=None passed by the user.

  4. florimondmanca commented on May 29, 2020

    @florimondmanca
    Contributor

    Looks like Requests behaves the same as we do for the specific case of auth, i.e. request(auth=None) is treated as "use the auth value on the session".

    >>> import requests
    >>> s = requests.Session()
    >>> s.auth = ("john", "admin")
    >>> url = "https://httpbin.org/headers"
    >>> r = s.get(url, auth=None)
    >>> r.json()["headers"]["Authorization"]
    'Basic am9objphZG1pbg=='

    But honestly I'd consider it a usability bug too.

    Eg we have timeout=None meaning "no timeout", so it'd be okay to assume that auth=None should mean "no auth".

    Do we see an issue with considering using the same UNSET trick as we did for timeouts here (and maybe on other parameters such as headers, cookies, params)?

  5. florimondmanca commented on May 29, 2020

    @florimondmanca
    Contributor

    I was going to suggest .build_request()/.send() as a workaround, but as expected it won't work either — the "use client auth if request auth is None" logic is made inside .send().

  6. cansarigol commented on May 30, 2020

    @cansarigol
    Contributor

    Can we use a decorator to evaluate signature data before set?

    import functools
    
    def auth_unset(func):
        @functools.wraps(func)
        def wrapper(*args,**kwargs):
            if 'auth' in kwargs and kwargs['auth'] is None:
            kwargs['auth'] = UNSET
        return func(*args,**kwargs)
    return wrapper
    
    @auth_unset
    def get(
        self,
        url: URLTypes,
    ...
    
  7. added this to the v1.0 milestone on Aug 7, 2020
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

    user-experienceEnsuring that users have a good experience using the library

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions