Quickstart¶
Eager to get started? This page gives a good introduction to getting started with Niquests.
First, make sure that:
Niquests is installed
Niquests is up-to-date
Let’s get started with some simple examples.
Note
Standalone async examples must be enclosed in an async function and started
with asyncio.run. Short async snippets below may be pasted into the
body of the documented main() wrapper.
import asyncio import niquests async def main() -> None: """Paste the example code here.""" if __name__ == "__main__": asyncio.run(main())
Make a request¶
Making a request with Niquests is very simple.
Begin by importing the Niquests module:
Now, let’s try to get a webpage. For this example, let’s get GitHub’s public timeline.
r = niquests.get('https://api.github.com/events')
r = await niquests.aget('https://api.github.com/events')
Now, we have a Response object called r. We can
get all the information we need from this object.
Niquests’ simple API makes all forms of HTTP requests straightforward. For example, this is how you make an HTTP POST request:
r = niquests.post('https://httpbingo.org/post', data={'key': 'value'})
r = await niquests.apost('https://httpbingo.org/post', data={'key': 'value'})
Nice, right? What about the other HTTP request methods: PUT, PATCH, DELETE, HEAD, OPTIONS, and QUERY? These are all just as simple:
r = niquests.put('https://httpbingo.org/put', data={'key': 'value'}) r = niquests.patch('https://httpbingo.org/patch', data={'key': 'value'}) r = niquests.delete('https://httpbingo.org/delete') r = niquests.head('https://httpbingo.org/get') r = niquests.options('https://httpbingo.org/get') r = niquests.query('https://httpbingo.org/anything', json={'key': 'value'})
r = await niquests.aput('https://httpbingo.org/put', data={'key': 'value'}) r = await niquests.apatch('https://httpbingo.org/patch', data={'key': 'value'}) r = await niquests.adelete('https://httpbingo.org/delete') r = await niquests.ahead('https://httpbingo.org/get') r = await niquests.aoptions('https://httpbingo.org/get') r = await niquests.aquery('https://httpbingo.org/anything', json={'key': 'value'})
QUERY, standardized by RFC 10008, carries a query in the request body.
Like GET, it is safe and idempotent; unlike GET, it can carry content.
Use Session.query() or
AsyncSession.query() when working with a session.
That’s all well and good, but it’s also only the start of what Niquests can do.
Passing parameters in URLs¶
You often want to send some sort of data in the URL’s query string. If
you were constructing the URL by hand, this data would be given as key/value
pairs in the URL after a question mark, e.g. httpbingo.org/get?key=val.
Niquests allows you to provide these arguments as a dictionary of strings,
using the params keyword argument. As an example, if you wanted to pass
key1=value1 and key2=value2 to httpbingo.org/get, you would use the
following code:
payload = {'key1': 'value1', 'key2': 'value2'} r = niquests.get('https://httpbingo.org/get', params=payload)
payload = {'key1': 'value1', 'key2': 'value2'} r = await niquests.aget('https://httpbingo.org/get', params=payload)
You can see that the URL has been correctly encoded by printing the URL:
print(r.url) # 'https://httpbingo.org/get?key1=value1&key2=value2'
Note that any dictionary key whose value is None will not be added to the
URL’s query string.
You can also pass a list of items as a value:
payload = {'key1': 'value1', 'key2': ['value2', 'value3']} r = niquests.get('https://httpbingo.org/get', params=payload) print(r.url) # 'https://httpbingo.org/get?key1=value1&key2=value2&key2=value3'
payload = {'key1': 'value1', 'key2': ['value2', 'value3']} r = await niquests.aget('https://httpbingo.org/get', params=payload) print(r.url) # 'https://httpbingo.org/get?key1=value1&key2=value2&key2=value3'
Response content¶
We can read the content of the server’s response. Consider the GitHub timeline again:
import niquests r = niquests.get('https://api.github.com/events') print(r.text) # '[{"repository":{"open_issues":0,"url":"https://github.com/...
import niquests r = await niquests.aget('https://api.github.com/events') print(r.text) # '[{"repository":{"open_issues":0,"url":"https://github.com/...
Niquests automatically decodes content from the server. Most Unicode character sets are decoded seamlessly.
When you make a request, Niquests makes educated guesses about the encoding of
the response based on the HTTP headers. The text encoding guessed by Niquests
is used when you access r.text. You can inspect and
change it through the r.encoding property:
print(r.encoding) # 'utf-8' r.encoding = 'ISO-8859-1' # Force a specific encoding.
Warning
If Niquests cannot decode the content to a string with confidence,
it returns None.
If you change the encoding, Niquests will use the new value of
r.encoding whenever you access
r.text. You might do this when
you can apply special logic to work out what the encoding of the content will
be. For example, HTML and XML can specify their encoding in their bodies. In
situations like this, use r.content to find the
encoding, and then set r.encoding. This will let
you use r.text with
the correct encoding.
Niquests will also use custom encodings if you need them. If
you have created your own encoding and registered it with the codecs
module, you can simply use the codec name as the value of
r.encoding and
Niquests will handle the decoding for you.
Binary response content¶
You can also access the response body as bytes, for non-text requests:
>>> r.content b'[{"repository":{"open_issues":0,"url":"https://github.com/...
The gzip and deflate content codings are automatically decoded for you.
The br content coding is automatically decoded for you if a Brotli library
like brotli or brotlicffi is installed.
The zstd content coding is automatically decoded on Python 3.14 and later using
the standard-library compression.zstd module. On older Python versions, install
the zstandard library or the niquests[zstd]
extra to enable it.
For example, to create an image from binary data returned by a request, you can use the following code:
>>> from PIL import Image >>> from io import BytesIO >>> i = Image.open(BytesIO(r.content))
JSON response content¶
There’s also a built-in JSON decoder for JSON data:
import niquests r = niquests.get('https://api.github.com/events') print(r.json()) # [{'repository': {'open_issues': 0, 'url': 'https://github.com/...
import niquests r = await niquests.aget('https://api.github.com/events') print(r.json()) # [{'repository': {'open_issues': 0, 'url': 'https://github.com/...
In case the JSON decoding fails, r.json() raises an exception. For example, if
the response gets a 204 (No Content), or if the response contains invalid JSON,
attempting r.json() raises
JSONDecodeError. This wrapper exception
provides interoperability for multiple exceptions that may be thrown by different
Python versions and JSON serialization libraries.
Response.json() attempts
to parse the response body regardless of its Content-Type header.
The success of a call to r.json() does not
indicate the success of the response. Some servers may return a JSON object in a
failed response (e.g. error details with HTTP 500). Such JSON will be decoded
and returned. To check that a request is successful, use
r.raise_for_status() or check that
r.status_code is what you expect.
Note
Since Niquests 3.2,
r.raise_for_status() is chainable because it
returns the response when no error is raised.
Tip
Niquests supports using orjson instead of the json standard
library. To use that feature, install orjson or niquests[speedups].
This can dramatically improve performance.
Tip
For typed JSON deserialization (e.g. with msgspec, pydantic, or cattrs),
use r.content directly instead of
r.json() for significantly better performance.
For example, msgspec.json.decode(r.content, type=list[User]) decodes bytes into typed
objects in a single pass, avoiding the intermediate dict. This is 2-5x faster than
msgspec.convert(r.json(), list[User]).
Raw response content¶
In the rare case that you’d like to get the raw socket response from the
server, you can access r.raw. If you want to do this,
make sure you set
stream=True in your initial request. Once you do, you can do this:
r = niquests.get('https://api.github.com/events', stream=True) r.raw # <urllib3.response.HTTPResponse object at ...> r.raw.read(10) # b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'
r = await niquests.aget('https://api.github.com/events', stream=True) r.raw # <urllib3._async.response.AsyncHTTPResponse object at ...> await r.raw.read(10) # b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'
In general, however, you should use a pattern like this to save what is being streamed to a file:
with open(filename, 'wb') as fd: for chunk in r.iter_content(chunk_size=128): fd.write(chunk)
with open(filename, 'wb') as fd: async for chunk in await r.iter_content(chunk_size=128): fd.write(chunk)
Warning
Consider aiofiles or a similar library to avoid blocking
file I/O in async code.
Using Response.iter_content will handle a lot
of what you would otherwise have to handle when using
Response.raw directly. When streaming a
download, the above is the preferred and recommended way to retrieve the
content. Note that chunk_size can be freely adjusted to a number that
may better fit your use cases.
More complicated POST requests¶
Typically, you want to send form-encoded data, much like an HTML form.
To do this, simply pass a dictionary to the data argument. Your
dictionary of data will automatically be form-encoded when the request is made:
payload = {'key1': 'value1', 'key2': 'value2'} r = niquests.post('https://httpbingo.org/post', data=payload) print(r.json()['form']) # {'key1': ['value1'], 'key2': ['value2']}
payload = {'key1': 'value1', 'key2': 'value2'} r = await niquests.apost('https://httpbingo.org/post', data=payload) print(r.json()['form']) # {'key1': ['value1'], 'key2': ['value2']}
The data argument can also have multiple values for each key. This can be
done by making data either a list of tuples or a dictionary with lists
as values. This is particularly useful when the form has multiple elements that
use the same key:
payload_tuples = [('key1', 'value1'), ('key1', 'value2')] r1 = niquests.post('https://httpbingo.org/post', data=payload_tuples) payload_dict = {'key1': ['value1', 'value2']} r2 = niquests.post('https://httpbingo.org/post', data=payload_dict) assert r1.json()['form'] == r2.json()['form']
payload_tuples = [('key1', 'value1'), ('key1', 'value2')] r1 = await niquests.apost('https://httpbingo.org/post', data=payload_tuples) payload_dict = {'key1': ['value1', 'value2']} r2 = await niquests.apost('https://httpbingo.org/post', data=payload_dict) assert r1.json()['form'] == r2.json()['form']
There are times that you may want to send data that is not form-encoded. If you pass in a string instead of a dictionary, that data will be posted directly.
For example, to encode JSON manually:
import json url = 'https://httpbingo.org/post' payload = {'some': 'data'} r = niquests.post(url, data=json.dumps(payload))
import json url = 'https://httpbingo.org/post' payload = {'some': 'data'} r = await niquests.apost(url, data=json.dumps(payload))
The code above does not add a Content-Type: application/json header.
If you need that header set and do not want to encode the dictionary yourself,
you can pass the object directly using the json parameter, and it will be
encoded automatically:
r = niquests.post(url, json=payload)
r = await niquests.apost(url, json=payload)
The json parameter is ignored if non-empty data or files is passed.
Custom JSON serialization¶
Use json_encoder to customize request-side JSON serialization. The encoder
is synchronous, even with async APIs, and must return str or bytes. For
example, msgspec can serialize supported
objects directly to bytes:
import msgspec import niquests class User(msgspec.Struct): name: str active: bool payload = User(name="Lina", active=True) encoder = msgspec.json.encode url = "https://httpbingo.org/post" r = niquests.post(url, json=payload, json_encoder=encoder) with niquests.Session(json_encoder=encoder) as session: r = session.post(url, json=payload)
import msgspec import niquests class User(msgspec.Struct): name: str active: bool payload = User(name="Lina", active=True) encoder = msgspec.json.encode url = "https://httpbingo.org/post" r = await niquests.apost(url, json=payload, json_encoder=encoder) async with niquests.AsyncSession(json_encoder=encoder) as session: r = await session.post(url, json=payload)
Top-level calls create a temporary session, so json_encoder can be passed directly
to post(), query(),
apost(), or aquery(). A session-level
encoder applies to every request made through that session.
POST multipart form data without a file¶
Since Niquests 3.1.2, you can override the default
application/x-www-form-urlencoded encoding and submit multipart form data
without a file:
r = niquests.post( url, data=payload, headers={'Content-Type': 'multipart/form-data'} )
r = await niquests.apost( url, data=payload, headers={'Content-Type': 'multipart/form-data'} )
Note
You can specify a boundary in the header value. Niquests reuses that boundary; otherwise, it generates one.
POST a multipart-encoded file¶
Niquests makes it simple to upload multipart-encoded files:
with open('report.xls', 'rb') as report: r = niquests.post(url, files={'file': report})
with open('report.xls', 'rb') as report: r = await niquests.apost(url, files={'file': report})
Warning
Opening a local file is blocking. Use an async file library when blocking file I/O is unsuitable for your application.
You can set the filename, content type, and headers explicitly:
with open('report.xls', 'rb') as report: files = { 'file': ( 'report.xls', report, 'application/vnd.ms-excel', {'Expires': '0'} ) } r = niquests.post(url, files=files)
with open('report.xls', 'rb') as report: files = { 'file': ( 'report.xls', report, 'application/vnd.ms-excel', {'Expires': '0'} ) } r = await niquests.apost(url, files=files)
You can also send strings as files:
files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')} r = niquests.post(url, files=files)
files = {'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n')} r = await niquests.apost(url, files=files)
In the event you are posting a very large file as a multipart/form-data
request, you may want to stream the request. By default, Niquests does not
provide a multipart streaming encoder, but a separate package does:
requests-toolbelt. You should read the toolbelt’s documentation for more details about how to use it.
For information about sending multiple files in one request, see the advanced section.
Response status codes¶
We can check the response status code:
>>> r = niquests.get('https://httpbingo.org/get') >>> r.status_code 200
Niquests also comes with a built-in status code lookup object for easy reference:
>>> r.status_code == niquests.codes.ok True
If a request returns a 4xx client error or 5xx server error response, we can
raise an exception with
Response.raise_for_status():
>>> bad_r = niquests.get('https://httpbingo.org/status/404') >>> bad_r.status_code 404 >>> try: ... bad_r.raise_for_status() ... except niquests.exceptions.HTTPError: ... print('The response was unsuccessful') The response was unsuccessful
Because the status_code for r is 200,
raise_for_status returns
the response itself:
>>> r.raise_for_status() is r True
All is well.
Redirection and history¶
By default, Niquests follows redirects for all methods except HEAD.
We can use the history property of the response object to track redirects.
The Response.history list contains the
Response objects that were created in order to
complete the request. The list is sorted from the oldest to the most recent
response.
For example, GitHub redirects all HTTP requests to HTTPS:
>>> r = niquests.get('http://github.com/') >>> r.url 'https://github.com/' >>> r.status_code 200 >>> r.history [<Response HTTP/2 [301]>]
If you’re using GET, OPTIONS, POST, PUT, PATCH, DELETE, or QUERY, you can disable
redirection handling with the allow_redirects parameter:
>>> r = niquests.get('http://github.com/', allow_redirects=False) >>> r.status_code 301 >>> r.history []
If you’re using HEAD, you can enable redirection as well:
>>> r = niquests.head('http://github.com/', allow_redirects=True) >>> r.url 'https://github.com/' >>> r.history [<Response HTTP/2 [301]>]
The redirect controls and history property are identical in async code:
r = await niquests.aget('http://github.com/', allow_redirects=False) assert r.status_code == 301 assert r.history == [] r = await niquests.ahead('http://github.com/', allow_redirects=True) assert r.url == 'https://github.com/' assert len(r.history) == 1
Timeouts¶
You can limit how long Niquests waits during network operations with the
timeout parameter. Nearly all production requests should specify a timeout:
try: niquests.get('https://github.com/', timeout=0.001) except niquests.exceptions.Timeout: print('The request timed out')
try: await niquests.aget('https://github.com/', timeout=0.001) except niquests.exceptions.Timeout: print('The request timed out')
Note
A scalar timeout applies to socket connection and read operations; it is
not a wall-clock limit on the entire response download. A read timeout is
raised when no response bytes arrive on the socket within that interval.
You can also pass a (connect, read) tuple. Top-level GET, HEAD, and
OPTIONS calls default to 30 seconds; write-oriented methods, including
QUERY, default to 120 seconds. A session’s timeout= value supplies its
default when individual session requests omit one.
Warning
Connection timeout behavior can be surprising when a host resolves to multiple addresses. Connection attempts may apply the timeout to each address in turn, so the elapsed wall-clock time can exceed the configured value. For example, two unreachable addresses can take roughly twice the connection timeout.
Tip
Set happy_eyeballs=True when constructing your Session to try all endpoints simultaneously.
This can reduce delays caused by unreachable addresses.
Warning
Python’s synchronous system resolver cannot always enforce this timeout if
system DNS is unresponsive. This limitation does not apply to async mode.
To avoid it, configure a custom resolver with resolver=; see
DNS Resolution below.
Errors and exceptions¶
In the event of a network problem, such as a DNS failure or refused connection,
Niquests will raise a ConnectionError exception.
Response.raise_for_status() will
raise an HTTPError if the HTTP request
returned an unsuccessful status code.
If a request times out, a Timeout exception is
raised.
If a request exceeds the configured maximum number of redirects, a
TooManyRedirects exception is raised.
All exceptions that Niquests explicitly raises inherit from
niquests.exceptions.RequestException.
HTTP/3 over QUIC¶
Niquests relies on urllib3.future and the semi-optional qh3 package for HTTP/3.
If qh3 is not installed, HTTP/3 and QUIC are unavailable, but HTTP/1.1 and
HTTP/2 continue to work. Installing qh3 may require a compilation toolchain.
Run python -m niquests.help to check whether the installed dependencies
support HTTP/3. You can inspect the protocol negotiated for a response:
r = niquests.get('https://1.1.1.1') print(r.http_version)
r = await niquests.aget('https://1.1.1.1') print(r.http_version)
The underlying library understands the Alt-Svc header and looks for an
h3 alternative service. Once a valid service is discovered, Niquests can
open a QUIC connection and caches that information in memory. Negotiation
depends on the peer, DNS and Alt-Svc information, cached state, network
conditions, and installed dependencies. Repeated calls do not guarantee a
particular HTTP/2-to-HTTP/3 sequence.
Note
With urllib3.future 2.4 or later, Niquests can negotiate HTTP/3 without a preceding TCP connection when the peer advertises HTTP/3 in an HTTPS DNS record.
Lazy responses and manual scheduling¶
HTTP/2 and HTTP/3 multiplexing is automatic in Niquests and does not require
this option. The historically named multiplexed=True option instead enables
manual response scheduling.
In this mode, each request is submitted immediately, but its request method returns a lazy, promise-backed response before waiting for the exchange to complete. This lets you submit several requests before resolving any of their responses.
When the peer supports HTTP/2 or HTTP/3, the outstanding exchanges can progress concurrently as independent streams on the same connection. Leaving one response unresolved does not prevent later requests from using that connection.
To benefit from this mode, submit multiple requests before accessing response
data. Resolve the resulting promises explicitly with
Session.gather() or
through the other resolution mechanisms described below.
Note
The parameter name is historical. multiplexed=True does not enable
HTTP/2 or HTTP/3, and it does not change protocol negotiation. It enables
lazy responses and gives the caller control over response resolution.
Submit requests¶
Request methods return public Response objects whose
lazy property is initially True. Internally, each lazy response is
backed by a response promise.
from niquests import Session with Session(multiplexed=True) as s: responses = [ s.get("https://httpbingo.org/delay/3"), s.get("https://httpbingo.org/delay/1"), ] assert all(response.lazy for response in responses) s.gather() # Resolve pending responses before closing the session.
from niquests import AsyncSession async with AsyncSession(multiplexed=True) as s: responses = [ await s.get("https://httpbingo.org/delay/3"), await s.get("https://httpbingo.org/delay/1"), ] assert all(response.lazy for response in responses) await s.gather() # Resolve pending responses before closing the session.
The final gather calls above are included for deterministic cleanup. The
following sections show how to choose which responses to resolve.
Resolve all responses¶
Calling gather without response arguments resolves every response pending
on the session:
After resolution, response.lazy is False and response attributes, body
methods, and extension APIs are available normally.
Resolve selected responses¶
Pass one or more lazy responses to gather to resolve only those promises:
from niquests import Session with Session(multiplexed=True) as s: responses = [ s.get("https://httpbingo.org/delay/3"), s.get("https://httpbingo.org/delay/1"), ] s.gather(responses[0]) print(responses[0].status_code) assert responses[1].lazy is True s.gather(responses[1])
from niquests import AsyncSession async with AsyncSession(multiplexed=True) as s: responses = [ await s.get("https://httpbingo.org/delay/3"), await s.get("https://httpbingo.org/delay/1"), ] await s.gather(responses[0]) print(responses[0].status_code) assert responses[1].lazy is True await s.gather(responses[1])
This allows application code to choose the order in which promise-backed responses are resolved.
Implicit resolution in synchronous code¶
In synchronous code, directly accessing response data implicitly resolves that response:
with Session(multiplexed=True) as s: response = s.get("https://httpbingo.org/delay/1") print(response.status_code) # Resolves this response first.
This implicit behavior is intentionally unavailable for non-awaitable
attributes on a lazy AsyncResponse, because blocking there
would stall the event loop. Resolve it explicitly with await s.gather(...).
Scheduling limits with max_fetch¶
Both session types expose
gather(*responses, max_fetch=None).
max_fetch limits how many available promises each mounted adapter resolves
during that call.
Here are some possible invocations:
s.gather() # Resolve all pending responses. s.gather(resp) # Resolve only resp. s.gather(max_fetch=2) # Resolve up to two available responses per adapter. s.gather(resp_a, resp_b, resp_c) # Resolve these three responses. s.gather(resp_a, resp_b, resp_c, max_fetch=1) # Resolve one available response per adapter.
await s.gather() # Resolve all pending responses. await s.gather(resp) # Resolve only resp. await s.gather(max_fetch=2) # Resolve up to two available responses per adapter. await s.gather(resp_a, resp_b, resp_c) # Resolve these three responses. await s.gather(resp_a, resp_b, resp_c, max_fetch=1) # Resolve one available response per adapter.
Async session¶
Niquests provides AsyncSession for awaitable HTTP requests.
Its request methods mirror Session and return coroutines.
Here is a basic example:
import asyncio from niquests import AsyncSession, Response async def main() -> None: async with AsyncSession() as s: tasks = [s.get("https://httpbingo.org/delay/1") for _ in range(10)] responses = await asyncio.gather(*tasks) print(responses) if __name__ == "__main__": asyncio.run(main())
Warning
Niquests currently supports only asyncio as its async backend.
Async lazy responses and manual scheduling¶
AsyncSession(multiplexed=True) uses the same manual response scheduling
described above. It does not enable HTTP/2 or HTTP/3 multiplexing; protocol
negotiation remains automatic.
Each awaited request method submits its request and returns a lazy,
promise-backed response without waiting for that exchange to finish. Submit
several requests before resolving them so HTTP/2 or HTTP/3 streams can progress
concurrently on the underlying connection. Resolve the promises explicitly with
await session.gather().
Look at this basic sample:
import asyncio from niquests import AsyncSession async def main() -> None: async with AsyncSession(multiplexed=True) as s: responses = [ await s.get("https://httpbingo.org/delay/1") for _ in range(10) ] assert all(response.lazy for response in responses) await s.gather() assert all(response.lazy is False for response in responses) print(responses) if __name__ == "__main__": asyncio.run(main())
Unlike synchronous lazy responses, an asynchronous lazy response cannot resolve itself while a non-awaitable attribute is being accessed, because doing so would block the event loop. Gather it explicitly before that access.
Warning
Combining AsyncSession with multiplexed=True
and stream=True produces a lazy AsyncResponse. Call
await session.gather() before directly accessing its non-awaitable
attributes or methods.
AsyncResponse for streams¶
Delaying the content consumption in an async context can be easily achieved using:
import niquests import asyncio async def main() -> None: async with niquests.AsyncSession() as s: r = await s.get("https://httpbingo.org/get", stream=True) async for chunk in await r.iter_content(16): print(chunk) if __name__ == "__main__": asyncio.run(main())
Or use iter_lines. It is an async generator, so iterate over it directly
without await:
import niquests import asyncio async def main() -> None: async with niquests.AsyncSession() as s: r = await s.get("https://httpbingo.org/get", stream=True) async for line in r.iter_lines(): print(line) if __name__ == "__main__": asyncio.run(main())
Or simply by doing:
import niquests import asyncio async def main() -> None: async with niquests.AsyncSession() as s: r = await s.get("https://httpbingo.org/get", stream=True) payload = await r.json() if __name__ == "__main__": asyncio.run(main())
When you specify stream=True with AsyncSession, the
returned object is an AsyncResponse. Its
iter_content and
iter_raw methods are awaitable and return async iterators, so use
async for chunk in await response.iter_content(). In contrast,
iter_lines is an async generator and is used as
async for line in response.iter_lines() without await. The
content, json,
text, and close
interfaces are also awaitable.
When enabling multiplexing in an async context, call await s.gather() before
direct access to non-awaitable response interfaces.
Here is a basic example of how you would do it:
import niquests import asyncio async def main() -> None: responses = [] async with niquests.AsyncSession(multiplexed=True) as s: responses.append( await s.get("https://httpbingo.org/get", stream=True) ) responses.append( await s.get("https://httpbingo.org/get", stream=True) ) print(responses) await s.gather() print(responses) for response in responses: async for chunk in await response.iter_content(16): print(chunk) if __name__ == "__main__": asyncio.run(main())
Warning
Accessing a non-awaitable attribute or method of a lazy
AsyncResponse without first calling await s.gather()
raises an error.
Scale your Session / pool¶
By default, Niquests retains up to 10 origin pools and configures each pool with a capacity of 10 connections. You can increase or decrease these values.
Set the following parameters in a session constructor:
Session(pool_connections=10, pool_maxsize=10)
pool_connectionsis the number of origin connection pools retained in the cache.pool_maxsizeis the configured connection capacity of each origin pool.
Tip
HTTP/2 and HTTP/3 can carry many concurrent streams over one connection, subject to the peer’s advertised stream limit.
Note
These settings are most useful for multithreaded or async applications.
Pool connections¶
After requests to three distinct origins, pool_connections=2 retains the two
most recently used origin pools, for host-b.tld and host-c.tld. The idle
pool for host-a.tld is evicted from the cache.
import niquests with niquests.Session(pool_connections=2) as s: s.get("https://host-a.tld/some") s.get("https://host-b.tld/some") s.get("https://host-c.tld/some")
import niquests async with niquests.AsyncSession(pool_connections=2) as s: await s.get("https://host-a.tld/some") await s.get("https://host-b.tld/some") await s.get("https://host-c.tld/some")
Attention
For backward compatibility, this cache size applies per mounted adapter.
The default HTTP and HTTPS adapters each retain up to two origin pools when
pool_connections=2, so pools for up to four origins may be retained
across both schemes.
Pool maxsize¶
Setting pool_maxsize=2 configures a capacity of two connections for the
host-a.tld origin pool. This setting matters primarily in concurrent async
or threaded environments.
DNS resolution¶
Niquests has built-in support for DNS over HTTPS, DNS over TLS, DNS over UDP, and DNS over QUIC. Encrypted resolvers use the configured trust store for certificate validation.
This feature uses the native urllib3.future implementation. The security properties of a custom resolver depend on the chosen transport and provider. DNSSEC validation is available when supported and enabled by the resolver implementation and provider.
Specify your own resolver¶
To specify a resolver, use a Session or
AsyncSession. Each session can have a different resolver.
This example uses Google Public DNS over HTTPS:
from niquests import Session with Session(resolver="doh+google://") as s: resp = s.get("https://httpbingo.org/get")
from niquests import AsyncSession async with AsyncSession(resolver="doh+google://") as s: resp = await s.get("https://httpbingo.org/get")
Here, httpbingo.org is resolved using the configured provider.
Note
By default, Niquests uses the system resolver.
The resolver argument also accepts public urllib3.future resolver
configuration objects. Use
niquests.packages.urllib3.ResolverDescription
with Session and
AsyncResolverDescription with
AsyncSession when you need to configure resolver fields
programmatically. See advanced for detailed resolver and
TLSConfiguration examples. TLSConfiguration can be passed
as tls_configuration= to top-level sync/async calls or either session type
to select a TLS backend, protocol versions, ciphers, or hostname policy.
Use multiple resolvers¶
You may specify a list of resolvers to be tested in the listed order.
from niquests import Session with Session(resolver=["doh+google://", "doh://cloudflare-dns.com"]) as s: resp = s.get("https://httpbingo.org/get")
from niquests import AsyncSession async with AsyncSession(resolver=["doh+google://", "doh://cloudflare-dns.com"]) as s: resp = await s.get("https://httpbingo.org/get")
The second entry, doh://cloudflare-dns.com, is tested only if
doh+google:// fails to provide a usable answer.
Note
In a multithreaded context, both resolvers may be used to improve performance.
Supported DNS URLs¶
Niquests supports a wide range of DNS protocols. Here are a few examples:
"doh+google://" # Shortcut for Google DNS over HTTPS. "dot+google://" # Shortcut for Google DNS over TLS. "doh+cloudflare://" # Shortcut for Cloudflare DNS over HTTPS. "doq+adguard://" # Shortcut for AdGuard DNS over QUIC. "dou://1.1.1.1" # DNS over UDP. "dou://1.1.1.1:8853" # DNS over UDP on port 8853. "doh://my-resolver.tld" # DNS over HTTPS with a custom server.
Set DNS via the environment¶
You can set the NIQUESTS_DNS_URL environment variable to the desired
resolver. It is used by every session that does not explicitly specify a
resolver.
Example:
export NIQUESTS_DNS_URL="doh://google.dns"
Disable DNS certificate verification¶
Add verify=false to the DNS URL. Disabling certificate verification is
unsafe and should be limited to controlled testing environments.
from niquests import Session with Session(resolver="doh+google://default/?verify=false") as s: resp = s.get("https://httpbingo.org/get")
from niquests import AsyncSession async with AsyncSession(resolver="doh+google://default/?verify=false") as s: resp = await s.get("https://httpbingo.org/get")
Warning
Doing a s.get("https://httpbingo.org/get", verify=False) does not impact the resolver.
Timeouts¶
You may set a specific timeout for domain name resolution by appending ?timeout=1 to the resolver configuration.
from niquests import Session with Session(resolver="doh+google://default/?timeout=1") as s: resp = s.get("https://httpbingo.org/get")
from niquests import AsyncSession async with AsyncSession(resolver="doh+google://default/?timeout=1") as s: resp = await s.get("https://httpbingo.org/get")
This prevents an individual DNS operation from waiting longer than one second.
Happy Eyeballs¶
New in version 3.5.5.
The underlying urllib3.future library provides Happy Eyeballs behind one option.
Happy Eyeballs (also called Fast Fallback) is an algorithm published by the IETF that makes dual-stack applications (those that understand both IPv4 and IPv6) more responsive to users by attempting to connect using both IPv4 and IPv6 at the same time (preferring IPv6), thus minimizing common problems experienced by users with imperfect IPv6 connections or setups.
The name “Happy Eyeballs” uses “eyeball” to describe endpoints that represent human Internet users, as opposed to servers.
import niquests with niquests.Session(happy_eyeballs=True) as s: ...
import niquests async with niquests.AsyncSession(happy_eyeballs=True) as s: ...
A single happy_eyeballs=True option enables the algorithm.
Note
This also applies when a server yields multiple IPv4 addresses but no IPv6 addresses. Niquests connects concurrently to the presented addresses and uses the fastest endpoint.
OCSP requests for certificate revocation checks also use the configured Happy Eyeballs setting.
Warning
This feature is disabled by default. It may become the default in a future major release.
WebSockets¶
New in version 3.9: Requires the WebSocket extra: pip install niquests[ws].
WebSockets are a vital part of the web ecosystem alongside HTTP. Niquests provides an integrated interface to reduce the friction of connecting to a WebSocket server for the first time.
Quick start¶
The following example interacts with a basic, well-known echo server.
from niquests import Session with Session() as s: resp = s.get( "wss://echo.websocket.org", ) print(resp.status_code) # 101 Switching Protocols print(resp.extension.next_payload()) # Read the next server message. resp.extension.send_payload("Hello World") print(resp.extension.next_payload() == "Hello World") # True resp.extension.close()
from niquests import AsyncSession import asyncio async def main() -> None: async with AsyncSession() as s: resp = await s.get("wss://echo.websocket.org") # ... print(await resp.extension.next_payload()) # unpack the next message from server await resp.extension.send_payload("Hello World") print((await resp.extension.next_payload()) == "Hello World") # output True! await resp.extension.close() asyncio.run(main())
Warning
Without the extra installed, an exception indicates that the scheme is unsupported.
Note
Requests historically accepted only http:// and https://.
Niquests also accepts wss:// for WebSocket Secure and ws:// for
plaintext WebSocket.
Warning
If the server rejects the WebSocket upgrade,
resp.extension is
None. Check it before using extension methods when rejection is possible.
WebSocket and HTTP/2+¶
By default, Niquests negotiates WebSocket over HTTP/1.1. It can also use the
extended CONNECT mechanism from RFC 8441 over HTTP/2 or HTTP/3. Few servers
support WebSocket over a multiplexed connection; use a URL such as
wss+rfc8441://example.com to request this mode.
Warning
echo.websocket.org does not support WebSocket over HTTP/2.
Ping and pong¶
Pings sent by a server are answered automatically while Niquests reads from the socket
through
next_payload.
Niquests does not automatically send pings to
the server.
from niquests import Session with Session() as s: resp = s.get( "wss://echo.websocket.org", ) resp.extension.ping() # Send a ping to the WebSocket server.
from niquests import AsyncSession async with AsyncSession() as s: resp = await s.get( "wss://echo.websocket.org", ) await resp.extension.ping() # Send a ping to the WebSocket server.
You can use the elementary methods provided by Niquests to construct your own logic.
Binary and text messages¶
You may use
next_payload and
send_payload(...)
with str or bytes.
If next_payload
returns bytes, the message is binary. If it returns a
string, the message is text.
The same distinction applies to
send_payload(...):
strings produce text
messages, while bytes produce binary messages.
Warning
Niquests does not buffer incomplete messages. It returns each received chunk as is.
Note
If
next_payload
returns None, the remote peer has closed the
connection.
Others¶
Other features, including proxies, Happy Eyeballs, and thread/task safety, also apply to WebSocket connections. See the relevant sections for details.
Example with concurrency¶
The following example communicates with a WebSocket echo server. It uses a thread for reads and the main thread for writes.
from __future__ import annotations from niquests import Session, Response, ReadTimeout from threading import Thread from time import sleep def pull_message_from_server(my_response: Response) -> None: """Read messages here.""" iteration_counter = 0 while my_response.extension.closed is False: try: # Blocks for at most one second. message = my_response.extension.next_payload() if message is None: # server just closed the connection. exit. print("received goaway from server") return print(f"received message: '{message}'") except ReadTimeout: # if no message received within 1s pass sleep(1) # let some time for the write part to acquire the lock iteration_counter += 1 # Send a ping every four iterations. if iteration_counter % 4 == 0: my_response.extension.ping() print("ping sent") if __name__ == "__main__": with Session() as s: # connect to websocket server "echo.websocket.org" with timeout of 1s (both read and connect) resp = s.get("wss://echo.websocket.org", timeout=1) if resp.status_code != 101: exit(1) t = Thread(target=pull_message_from_server, args=(resp,)) t.start() # Send messages here. for i in range(30): to_send = f"Hello World {i}" resp.extension.send_payload(to_send) print(f"sent message: '{to_send}'") sleep(1) # let some time for the read part to acquire the lock # exit gently! resp.extension.close() # wait for thread proper exit. t.join() print("program ended!")
Warning
The sleeps give each side an opportunity to acquire the shared read/write lock and prevent starvation.
import asyncio from niquests import AsyncSession, ReadTimeout, Response async def read_from_ws(my_response: Response) -> None: iteration_counter = 0 while my_response.extension.closed is False: try: # Blocks for at most one second. message = await my_response.extension.next_payload() if message is None: # server just closed the connection. exit. print("received goaway from server") return print(f"received message: '{message}'") except ReadTimeout: # if no message received within 1s pass await asyncio.sleep(1) # let some time for the write part to acquire the lock iteration_counter += 1 # Send a ping every four iterations. if iteration_counter % 4 == 0: await my_response.extension.ping() print("ping sent") async def main() -> None: async with AsyncSession() as s: resp = await s.get("wss://echo.websocket.org", timeout=1) print(resp) task = asyncio.create_task(read_from_ws(resp)) for i in range(30): to_send = f"Hello World {i}" await resp.extension.send_payload(to_send) print(f"sent message: '{to_send}'") await asyncio.sleep(1) # let some time for the read part to acquire the lock # exit gently! await resp.extension.close() await task if __name__ == "__main__": asyncio.run(main())
Note
These examples are intentionally basic. Adjust their settings and algorithms to match your requirements.
Server-sent events (SSE)¶
New in version 3.11.2.
Server-Sent Events, commonly abbreviated SSE, provide a standard way to stream events continuously from a server to a client in real time.
Starting example¶
The native urllib3.future SSE extension manages a stream of events:
from __future__ import annotations import niquests if __name__ == "__main__": with niquests.Session() as s: r = s.post("sse://httpbingo.org/sse") print(r.status_code) while r.extension.closed is False: event: niquests.ServerSentEvent | None = r.extension.next_payload() print(event)
import niquests import asyncio async def main() -> None: async with niquests.AsyncSession() as s: r = await s.post("sse://httpbingo.org/sse") print(r) # output: <Response HTTP/2 [200]> while r.extension.closed is False: print(await r.extension.next_payload()) # ServerSentEvent(event='ping', data='{"id":0,"timestamp":1732857000473}') if __name__ == "__main__": asyncio.run(main())
The sse:// scheme indicates the intent to consume an SSE endpoint.
Note
sse:// uses https:// underneath. For an unencrypted
connection, use psse://.
The interface resembles the WebSocket implementation, except that
next_payload
returns a ServerSentEvent object by default.
Interrupt the stream¶
A server may send events forever. Always close the SSE extension when you stop consuming it so the remote peer is notified.
For example, sse://sse.dev/test sends events until the client stops it.
See how to stop cleanly the flow of events:
import niquests if __name__ == "__main__": with niquests.Session() as s: r = s.post("sse://sse.dev/test") events = [] while r.extension.closed is False: event = r.extension.next_payload() if event is None: # The remote peer closed the stream. break events.append(event) # add the event to list if len(events) >= 10: # close ourselves SSE stream & notify remote peer. r.extension.close()
import niquests import asyncio async def main() -> None: async with niquests.AsyncSession() as s: r = await s.post("sse://sse.dev/test") events = [] while r.extension.closed is False: event = await r.extension.next_payload() if event is None: # The remote peer closed the stream. break events.append(event) # add the event to list if len(events) >= 10: # close ourselves SSE stream & notify remote peer. await r.extension.close() if __name__ == "__main__": asyncio.run(main())
ServerSentEvent¶
Note
next_payload
returns a ServerSentEvent by default, or
None when the server terminates the event stream.
This object represents one parsed event and provides these attributes and methods:
payload.json()to deserialize JSON datapayload.idfor the event IDpayload.datafor the raw message payloadpayload.eventfor the event type, such asmessageorpingpayload.retryfor the reconnection time
The full class source is located at https://github.com/jawah/urllib3.future/blob/3d7c5d9446880a8d473b9be4db0bcd419fb32dee/src/urllib3/contrib/webextensions/sse.py#L14
Notes¶
SSE can use HTTP/1.1, HTTP/2, or HTTP/3. Features such as proxies, Happy Eyeballs, and hooks remain available.
Unix sockets¶
New in version 3.17.0.
Warning
Unix domain sockets are available only on Linux and Unix-like systems. They support HTTP/1.1 and cleartext HTTP/2 (h2c), not HTTP/3.
Niquests natively supports connecting to services through Unix domain sockets. This is useful for local services, such as the Docker Engine API, databases, or any application that exposes an HTTP API over a Unix socket.
Basic usage¶
To connect via a Unix socket, use the http+unix:// scheme with the URL-encoded socket path:
from niquests import Session with Session() as s: # %2F is the URL-encoded forward slash response = s.get("http+unix://%2Fvar%2Frun%2Fdocker.sock/version") print(response.json())
from niquests import AsyncSession async with AsyncSession() as s: response = await s.get("http+unix://%2Fvar%2Frun%2Fdocker.sock/version") print(response.json())
Tip
Use the base_url parameter on a session to avoid repeatedly writing
http+unix://%2Fvar%2Frun%2Fdocker.sock/.
Warning
To use h2c over a Unix socket, disable HTTP/1.1 with
Session(disable_http1=True). Few services support this configuration.
URL format¶
The Unix socket URL follows this pattern:
http+unix://<url-encoded-socket-path>/<api-path>
For example, to access /var/run/docker.sock with path /version:
Socket path:
/var/run/docker.sockURL-encoded:
%2Fvar%2Frun%2Fdocker.sockFull URL:
http+unix://%2Fvar%2Frun%2Fdocker.sock/version
Tip
Use urllib.parse.quote(path, safe='') to URL-encode socket paths programmatically.
Concurrent connections¶
Unix sockets support multiple concurrent connections, just like TCP sockets:
from concurrent.futures import ThreadPoolExecutor from niquests import Session endpoints = ["/containers/json", "/images/json", "/version", "/info"] with Session() as s, ThreadPoolExecutor() as executor: responses = list(executor.map( lambda endpoint: s.get( f"http+unix://%2Fvar%2Frun%2Fdocker.sock{endpoint}" ), endpoints, ))
import asyncio from niquests import AsyncSession async with AsyncSession() as s: endpoints = ["/containers/json", "/images/json", "/version", "/info"] responses = await asyncio.gather(*( s.get(f"http+unix://%2Fvar%2Frun%2Fdocker.sock{endpoint}") for endpoint in endpoints ))
WebSocket and SSE¶
New in version 3.18.0.
WebSocket and SSE extensions are also available over Unix sockets:
import niquests with niquests.Session() as s: r = s.get("psse+unix://%2Ftmp%2Fhello.sock/sse") while not r.extension.closed: print(r.extension.next_payload()) ws = s.get("ws+unix://%2Ftmp%2Fhello.sock/ws") ws.extension.send_payload("Hello") print(ws.extension.next_payload()) ws.extension.close()
import niquests async with niquests.AsyncSession() as s: r = await s.get("psse+unix://%2Ftmp%2Fhello.sock/sse") while not r.extension.closed: print(await r.extension.next_payload()) ws = await s.get("ws+unix://%2Ftmp%2Fhello.sock/ws") await ws.extension.send_payload("Hello") print(await ws.extension.next_payload()) await ws.extension.close()
The psse+unix:// scheme, rather than http+unix://, tells Niquests to
initialize response.extension for SSE.
Use psse+unix:// for SSE and ws+unix:// for WebSocket. See the dedicated
sections above for their interfaces.
Warning
ws+unix:// requires you to have the ws extra installed.
WSGI/ASGI application testing¶
New in version 3.17.0.
Niquests provides built-in adapters for testing WSGI and ASGI applications directly without starting a server. This is particularly useful for integration testing.
Warning
In-process adapters ignore connection-specific settings such as HTTP version toggles, pool sizing, and multiplexing.
ASGI applications (async)¶
Test your FastAPI, Starlette, or other ASGI applications directly:
from fastapi import FastAPI, Request app = FastAPI() @app.get("/hello") async def hello(request: Request): return {"message": "hello from asgi"} @app.api_route("/echo", methods=["GET", "POST"]) async def echo(request: Request): body = await request.body() return {"body": body.decode()}
Basic usage:
import asyncio from niquests import AsyncSession async def main(): async with AsyncSession(app=app) as s: resp = await s.get("/hello?foo=bar") print(resp.status_code) # 200 print(resp.json()) # {"message": "hello from asgi"} asyncio.run(main())
Ordinary streaming responses:
async def main(): async with AsyncSession(app=app) as s: resp = await s.post("/echo", data=b"foobar", stream=True) body = b"" async for chunk in await resp.iter_content(6): body += chunk print(body) asyncio.run(main())
The async ASGI adapter exposes ordinary streamed response bodies through
AsyncResponse, as shown above. This is separate from the
WebSocket and SSE extension protocols below.
WebSocket and SSE:
New in version 3.18.0.
WebSocket and Server-Sent Events work with ASGI applications using the exact same interfaces as the main HTTP part.
Use wss:// (or ws://) for WebSocket and sse:// (or psse://) for SSE, just like you would with a live server.
from fastapi import FastAPI, WebSocket from starlette.responses import StreamingResponse app = FastAPI() @app.websocket("/ws-echo") async def ws_echo(websocket: WebSocket): await websocket.accept() while True: data = await websocket.receive_text() await websocket.send_text(f"echo: {data}") @app.get("/sse-events") async def sse_events(): async def generate(): for i in range(3): yield f"event: message\ndata: event {i}\n\n" return StreamingResponse(generate(), media_type="text/event-stream")
WebSocket:
from niquests import Session with Session(app=app) as s: resp = s.get("wss://default/ws-echo") resp.extension.send_payload("Hello") print(resp.extension.next_payload()) # "echo: Hello" resp.extension.close()
SSE:
from niquests import Session with Session(app=app) as s: resp = s.get("sse://default/sse-events") while not resp.extension.closed: event = resp.extension.next_payload() if event is None: break print(event) # ServerSentEvent(event='message', data='event 0')
import asyncio from niquests import AsyncSession async def main(): async with AsyncSession(app=app) as s: # WebSocket resp = await s.get("wss://default/ws-echo") await resp.extension.send_payload("Hello") print(await resp.extension.next_payload()) # "echo: Hello" await resp.extension.close() # SSE resp = await s.get("sse://default/sse-events") while not resp.extension.closed: event = await resp.extension.next_payload() if event is None: break print(event) asyncio.run(main())
Note
You can also use an ASGI app with a synchronous session. Ordinary
ASGI response bodies are buffered in that mode, so stream=True does not
provide incremental body delivery. WebSocket and SSE still stream through
response.extension as shown above. The synchronous adapter handles ASGI
lifespan startup and shutdown events automatically.
WSGI applications (sync)¶
Test your Flask, Django, or other WSGI applications:
from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/hello") def hello(): return jsonify({"message": "hello from wsgi"}) @app.route("/echo", methods=["GET", "POST"]) def echo(): return jsonify({"body": request.get_data(as_text=True)})
Basic usage:
from niquests import Session with Session(app=app) as s: resp = s.get("/hello?foo=bar") print(resp.status_code) # 200 print(resp.json()) # {"message": "hello from wsgi"}
Streaming responses:
with Session(app=app) as s: resp = s.post("/echo", data=b"foobar", stream=True) print(resp.json()) for chunk in resp.iter_content(6): ...
Server-Sent Events:
New in version 3.18.0.
SSE works with WSGI applications using the same interface as the main HTTP part.
from flask import Flask, Response app = Flask(__name__) @app.route("/sse-events") def sse_events(): def generate(): for i in range(3): yield f"event: message\ndata: event {i}\n\n" return Response(generate(), mimetype="text/event-stream")
from niquests import Session with Session(app=app) as s: resp = s.get("sse://default/sse-events") while not resp.extension.closed: event = resp.extension.next_payload() if event is None: break print(event) # ServerSentEvent(event='message', data='event 0')
Warning
The WSGI adapter is request/response-only and does not support WebSocket. Use an ASGI application for WebSocket testing.
Running as a WASI component¶
New in version 3.21.0.
WASI is an excellent fit for sandboxed applications, plug-ins, serverless functions, and edge deployments. A component starts without ambient access to the network or filesystem: its WIT world declares what it can use, and the host decides which of those capabilities to grant when it runs the component. The same component can therefore be deployed under different security policies without changing its Python code.
Tip
WASI can play a key role in deploying agents at scale. A library such as padwan-llm can help build agent loops in a constrained WASI runtime using Niquests.
For the most complete Niquests experience, prefer the WASI socket interfaces. Use
Preview 2 sockets for synchronous code and Preview 3 sockets for asynchronous code.
The matching wasi:cli/command world from the componentize-py source distribution
includes those interfaces.
Install Niquests with the Rustls backend so that HTTPS can run over WASI sockets, then download the matching componentize-py source archive:
$ python -m pip install "niquests[rtls]" "componentize-py==0.25.0" $ python -m pip download --no-deps --no-binary=:all: "componentize-py==0.25.0" $ python -m tarfile -e componentize_py-0.25.0.tar.gz .
Note
The PyPI wheel installs the componentize-py executable but does not include
its WASI WIT definitions. The source archive provides the required wit/
directory; keep both at the same pinned version.
Choose the execution model for your component:
Use Preview 2 sockets for a synchronous component:
import niquests from wit_world import exports class Run(exports.Run): def run(self) -> None: response = niquests.get("https://httpbingo.org/get", timeout=10) print(response.status_code)
Build against the Preview 2 command world, then grant network and DNS access:
$ componentize-py -d componentize_py-0.25.0/wit -w wasi:cli/command@0.2.0 componentize app -o app.wasm $ wasmtime run -Sinherit-network -Sallow-ip-name-lookup=y app.wasm
Use Preview 3 sockets for an asynchronous component:
import niquests from wit_world import exports class Run(exports.Run): async def run(self) -> None: response = await niquests.aget("https://httpbingo.org/get", timeout=10) print(response.status_code)
Build against the Preview 3 command world, then enable Preview 3, network, and DNS access:
$ componentize-py -d componentize_py-0.25.0/wit -w wasi:cli/command@0.3.0 componentize app -o app.wasm $ wasmtime run -Sp3 -Sinherit-network -Sallow-ip-name-lookup=y app.wasm
Warning
Request timeouts are currently not enforced when combining Preview 3 sockets with
componentize-py. Async cancellation is not implemented by componentize-py, so a
timeout value is accepted silently but does not interrupt the request or raise
a timeout exception. This limitation does not apply to WASI HTTP 0.3: its host
request options enforce connect, first-byte, and between-byte timeouts normally.
Important
-Sallow-ip-name-lookup=y is the DNS permission for WASI sockets. Without it,
inherit-network still exposes sockets, but URLs containing hostnames such as
httpbingo.org cannot be resolved. It is not required by a WIT HTTP-only
component: under -Shttp, the host HTTP service performs DNS on the component’s
behalf.
Tip
You can omit -Sallow-ip-name-lookup=y when using a custom Niquests resolver.
That permission specifically exposes name resolution through the host or parent
environment; it is not required for DNS carried over the component’s permitted
network sockets. This distinction matters for sandboxed workloads: parent DNS may
reveal private names and addresses from an internal namespace, such as Kubernetes
cluster DNS, even when the application only needs public Internet destinations.
In that situation, deny host name lookup and configure an explicit external resolver, such as DNS over HTTPS or TLS. Ensure that the resolver can be bootstrapped without host DNS, for example by addressing a trusted resolver by IP or supplying an otherwise pre-resolved endpoint. You may also use the in-memory resolver if that’s simpler.
The example grants network access and permission to resolve names, but no host
directory is preopened. Do not add --dir unless the application genuinely needs
host files. Be aware that Wasmtime’s inherit-network grant is intentionally broad:
it exposes the host network namespace rather than only the URL shown above. In
production, prefer a host-level destination allowlist over unrestricted inherited
network access when your runtime supports one.
Niquests selects the available WASI transport automatically; application code does not mount an adapter. Socket WIT provides native connection pooling, protocol negotiation, WebSocket, SSE, redirects, and the usual Session behavior. A host-managed WASI HTTP interface is also supported as a constrained fallback. See WASI transports and capabilities before choosing that contract or designing a least-authority deployment.
Note
Preview 3 and its componentize-py integration are still evolving. Pin your component toolchain and runtime together for reproducible deployments.
Running in the browser (Pyodide)¶
New in version 3.18.0.
Niquests runs natively in Pyodide without configuration
changes. HTTP requests, WebSocket, and SSE use Session,
AsyncSession, and
resp.extension. The adapter is selected
automatically when Pyodide is detected.
Warning
Synchronous interfaces require a JSPI-capable browser or Node.js runtime. Modern builds of Firefox, Chrome, and Node.js support JSPI.
# This exact code works in both CPython and Pyodide: import niquests resp = niquests.get("https://httpbingo.org/get") print(resp.json())
# This exact code works in both CPython and Pyodide: import niquests resp = await niquests.aget("https://httpbingo.org/get") print(resp.json())
Note
Although Niquests exposes synchronous HTTP interfaces, prefer
async/await in browsers, whose networking APIs are asynchronous.
WebSocket and SSE use the same API as described in the sections above:
from niquests import Session with Session() as s: # Behind the scenes, uses the browser's native WebSocket API resp = s.get("wss://echo.websocket.org") resp.extension.send_payload("Hello") print(resp.extension.next_payload()) resp.extension.close() # Behind the scenes, uses fetch streaming under the hood resp = s.get("sse://some-server.example/events") while not resp.extension.closed: event = resp.extension.next_payload() if event is None: break print(event)
from niquests import AsyncSession async with AsyncSession() as s: # Behind the scenes, uses the browser's native WebSocket API resp = await s.get("wss://echo.websocket.org") await resp.extension.send_payload("Hello") print(await resp.extension.next_payload()) await resp.extension.close() # Behind the scenes, uses fetch streaming under the hood resp = await s.get("sse://some-server.example/events") while not resp.extension.closed: event = await resp.extension.next_payload() if event is None: break print(event)
What the browser controls¶
Under Pyodide, the browser’s network stack handles the actual connections. Some behavior differs from ordinary CPython. These are properties of the browser sandbox rather than Niquests limitations:
DNS resolution is handled by the browser. Custom resolvers, DNS over HTTPS, and protocol toggles have no effect.
TLS is handled by the browser. The
verify,cert, andtls_configurationparameters are ignored, and the browser uses its own certificate store. Consequently,response.conn_infois unset.CORS applies. The remote server must include the appropriate
Access-Control-Allow-Originheaders, or the browser will block the request.response.http_versionisNone. The browser does not expose the negotiated HTTP protocol.Certain headers cannot be set. The browser forbids overriding
Host,Origin,Cookie,Connection, and other forbidden headers.No HTTP+Unix sockets. Unix domain sockets are not available in the browser environment.
Pool sizing, HTTP version toggles, and multiplexing settings are silently ignored because the browser manages its own connection pool.
Redirect history is unavailable through the browser sandbox.
Disabling automatic redirects is unsupported because intermediate responses are opaque to browser code.
Application-level proxy configuration is unavailable because the browser or operating system controls proxies outside WASM/JavaScript.
``pre_send`` and ``early_response`` hooks are silently ignored.
Extras such as
socks,ocsp,speedups, andzstdare unavailable or unused in WASM.
These restrictions apply to all code running inside the browser sandbox.
Scheme mapping¶
The scheme prefixes work exactly as elsewhere:
sse://maps tohttps://for SSEpsse://maps tohttp://for plaintext SSEwss://andws://use the browser’s native WebSocket
Ready for more? Check out the advanced section.