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

Support cookies with value None #3096

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

tducret
Copy link

@tducret tducret commented Feb 15, 2024

Summary

There are 3 kinds of request cookie headers.

request = httpx.Request("GET", "https://www.example.org", cookies=cookies)
request.headers["cookie"]  # <== This is the request cookie header we are talking about here

"name=value"

This is the classic case, already supported through :

cookies.set(name="name", value="value", domain="example.org")

"name="

Please note the trailing = sign here.
This is also supported, by passing an empty string as the cookie value :

cookies.set(name="name", value="", domain="example.org")

"name"

Please note there is no trailing = sign here.
This is not officially supported (yet), but can be achieved with :

cookies.set(name="name", value=None, domain="example.org")

It works because the cookie jar implementation in the standard library already deals with it.
Contrary to the httpx.Cookies.set method, the standard Cookie doesn't define strict typing for the value.

So this PR changes the httpx._models in order to support a cookie value which is None.
Otherwise the typing is incorrect, forcing the httpx to add a # type: ignore instruction.

The getter functions had to be updated as well, as the None was considered as a not found value.

2 new test cases are added in this PR to ensure the proper behavior.

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

@@ -1008,7 +1008,7 @@ async def aclose(self) -> None:
await self.stream.aclose()


class Cookies(typing.MutableMapping[str, str]):
class Cookies(typing.MutableMapping[str, typing.Optional[str]]):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class Cookies(typing.MutableMapping[str, typing.Optional[str]]):
class Cookies(typing.MutableMapping[str, str | None]):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @T-256 ! Actually, That was my initial attempt, but the test workflow failed with this notation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, no problem then...

@tducret
Copy link
Author

tducret commented Feb 20, 2024

Is there anything else I can do before we merge it to the main branch?

@tomchristie
Copy link
Member

Do name= and name have the same behavior?...

@tducret
Copy link
Author

tducret commented Feb 23, 2024

Do name= and name have the same behavior?...

According to the RFC for HTTP protocol :

If the name-value-pair string lacks a %x3D ("=") character, then the name string is empty, and the value string is the value of name-value-pair.

So it may depend on the server implementation, but I can confirm a case where the server considered name= and name as different.
Although this was already possible to create both kinds of cookies, this PR confirms this usage by changing the type hint.

@@ -1080,23 +1082,26 @@ def get( # type: ignore
default: str | None = None,
domain: str | None = None,
path: str | None = None,
raise_when_not_found: bool = False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid adding this API as part of the pull request?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, I wanted to preserve this responsibility of raising the KeyError exception to the __getitem__() method.

However, if it's safe to raise it whenever it's not found through get(), then we could remove raise_when_not_found parameter.

What do you think?


cookies.update(more_cookies)
assert dict(cookies) == {"no-value": None}
assert cookies.get("no-value", domain="example.com") is None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not absolutely clear to me that allowing None here is an API improvement. 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the test to assert cookies["no-value"] is None.
I also added test_get_missing_cookie_raises_exception to highlight the difference between getting a missing cookie (raises exception) and getting a cookie whose value is None.

Is it better?

It highlights the difference with a cookie whose value is `None`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants