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 'httpx.json = ...' #1352

Closed
wants to merge 13 commits into from
31 changes: 31 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,37 @@ class MyCustomAuth(httpx.Auth):
raise RuntimeError("Cannot use a sync authentication class with httpx.AsyncClient")
```

## Custom JSON libraries

By default, HTTPX uses the standard library `json` module when making requests with `json=...`.

It is possible to patch the JSON library used by HTTPX globally by setting `httpx.json`:

```python
import httpx
import orjson

# Globally switch out the JSON implementation used by HTTPX.
httpx.json = orjson
```

Any object that exposes `.loads(data: str) -> Any` and `.dumps(obj: Any) -> str` is accepted (for maximum compatibility, `.dumps(obj: Any) -> bytes` is also supported). For full control over the serialization behavior, you can build and provide your own implementation. This also allows overloading each method independently. For example:

```python
import uson
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
import orjson

# A JSON serializer that uses different libraries for encoding vs decoding.
class HybridJSON:
def loads(self, text):
return orgjson.loads(text)
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved

def dumps(self, data):
return ujson.dumps(data)

httpx.json = HybridJSON()
```

## SSL certificates

When making a request over HTTPS, HTTPX needs to verify the identity of the requested host. To do this, it uses a bundle of SSL certificates (a.k.a. CA bundle) delivered by a trusted certificate authority (CA).
Expand Down
3 changes: 3 additions & 0 deletions httpx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

from .__version__ import __description__, __title__, __version__
from ._api import delete, get, head, options, patch, post, put, request, stream
from ._auth import Auth, BasicAuth, DigestAuth
Expand Down Expand Up @@ -63,6 +65,7 @@
"HTTPError",
"HTTPStatusError",
"InvalidURL",
"json",
"Limits",
"LocalProtocolError",
"NetworkError",
Expand Down
12 changes: 10 additions & 2 deletions httpx/_content.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
from json import dumps as json_dumps
from typing import (
Any,
AsyncIterable,
Expand Down Expand Up @@ -138,7 +137,16 @@ def encode_html(html: str) -> Tuple[Dict[str, str], ByteStream]:


def encode_json(json: Any) -> Tuple[Dict[str, str], ByteStream]:
body = json_dumps(json).encode("utf-8")
import httpx

# Note that we allow 'json.dumps(...)' returning bytes. This isn't the
# stdlib behavior, but we support overriding the default implementation...
# eg. `httpx.json = ujson` and `httpx.json = orjson`
# And we'd like to gracefully support the case of 'orjson' which returns
# bytes directly.
body_str = httpx.json.dumps(json)
body = body_str if isinstance(body_str, bytes) else body_str.encode("utf-8")

content_length = str(len(body))
content_type = "application/json"
headers = {"Content-Length": content_length, "Content-Type": content_type}
Expand Down
7 changes: 4 additions & 3 deletions httpx/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import contextlib
import datetime
import email.message
import json as jsonlib
import typing
import urllib.request
from collections.abc import MutableMapping
Expand Down Expand Up @@ -1106,14 +1105,16 @@ def raise_for_status(self) -> None:
raise HTTPStatusError(message, request=request, response=self)

def json(self, **kwargs: typing.Any) -> typing.Any:
import httpx

if self.charset_encoding is None and self.content and len(self.content) > 3:
encoding = guess_json_utf(self.content)
if encoding is not None:
try:
return jsonlib.loads(self.content.decode(encoding), **kwargs)
return httpx.json.loads(self.content.decode(encoding), **kwargs)
except UnicodeDecodeError:
pass
return jsonlib.loads(self.text, **kwargs)
return httpx.json.loads(self.text, **kwargs)

@property
def cookies(self) -> "Cookies":
Expand Down
21 changes: 21 additions & 0 deletions tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,24 @@ def test_raw_client_header():
["User-Agent", f"python-httpx/{httpx.__version__}"],
["Example-Header", "example-value"],
]


def test_override_json():
class MockJSONLib:
def loads(self, *args, **kwargs):
return {"hello": "custom json lib"}

def dumps(self, *args, **kwargs):
return '{"hello": "custom json lib"}'

try:
httpx.json = MockJSONLib() # type: ignore

client = httpx.Client(transport=MockTransport(hello_world))
response = client.post("http://example.org/", json={"example": 123})

assert response.json() == {"hello": "custom json lib"}
finally:
import json

httpx.json = json