niquests Documentation

repository·main·Indexed 25 days ago

https://github.com/jawah/niquests

A high-performance, feature-rich Python HTTP library designed as a modern, drop-in replacement for Requests. Niquests supports HTTP/2, HTTP/3, asynchronous programming via async/await, and enhanced security features such as DNS over HTTPS and OCSP verification. It is compatible with Python/PyPy 3.7+, WASI, and Pyodide. The ecosystem includes extensions such as niquests-cache for response caching, niquests-mock for HTTP mocking, and opentelemetry-instrumentation-niquests for tracing and metrics.

Tokens
39.2K
Snippets
111
Records
189
Agent score
80%

What's inside niquests

  1. Overview of Niquests features

    main

    Niquests is designed for modern web requirements and provides several advanced capabilities compared to standard HTTP libraries:

    • Protocol Support: HTTP/1.1, HTTP/2 (with prior knowledge), and HTTP/3 over QUIC.
    • Advanced DNS: DNS over HTTPS (DoH), DNS over QUIC (DoQ), DNS over TLS (DoT), and DNS over UDP (DoU), including DNSSEC support.
    • Security: OS truststore by default (no certifi required), OCSP verification, Browser-style TLS/SSL verification, and Post-Quantum Security.
    • Modern Web Protocols: WebSocket support, Server-Sent Events (SSE) client, and HTTP/2 Multiplexing.
    • Developer Experience: Fully type-annotated, async support, SOCKS proxy support, and automatic content decompression/decoding.
    • Runtime Support: Python 3.7+, PyPy, Pyodide, and WASI.
  2. WASI Transports and Capabilities

    main

    Niquests supports WASI (WebAssembly System Interface) by performing capability discovery from wit_world bindings. It automatically selects the appropriate transport based on the available interfaces:

    • Synchronous Session: Prefers Preview 2 sockets. Can fallback to wasi:http@0.2.0 if sockets are absent.
    • Asynchronous AsyncSession: Prefers Preview 3 sockets. Can fallback to wasi:http@0.3.0 if sockets are absent.

    Socket WIT (Native Networking)

    Provides full control over the transport (pooling, TLS, DNS) inside the guest. Requires urllib3.future >= 2.24.900 and niquests[rtls] for HTTPS.

    • Wasmtime Grant: -Sinherit-network -Sallow-ip-name-lookup=y (and -Sp3 for Preview 3).

    WIT HTTP (High-level Capability)

    Submits requests to the host. The host manages DNS, TCP, and TLS. This is a smaller authority surface suitable for untrusted plugins.

    • Wasmtime Grant: -Shttp (and -Sp3 for Preview 3). No socket permissions required.
    • Limitations: Custom CA bundles, verify=False, custom DNS, and SOCKS proxies are unavailable because the host controls the TLS/TCP layer.
  3. Combine Unix Sockets with SSE or WebSocket

    main

    You can combine Unix socket connectivity with the SSE or WebSocket extensions by using specialized schemes. This allows you to boot the extension automatically.

    • For SSE over a Unix socket: Use psse+unix:// (uses https:// logic).
    • For WebSocket over a Unix socket: Use ws+unix:// (requires the ws extra to be installed).

    Note: Using psse+unix:// instead of http+unix:// tells Niquests to automatically start the response.extension for you.

    import niquests
    
    with niquests.Session() as s:
        # Automatically starts the SSE extension
        r = s.get("psse+unix://%2Ftmp%2Fhello.sock/sse")
        while not r.extension.closed:
            print(r.extension.next_payload())
  4. Handle encoded response data

    main

    Niquests automatically decompresses gzip-encoded responses and attempts to decode content to Unicode where possible.

    To enable automatic decoding of Brotli-encoded responses, ensure that either the brotli or brotlicffi package is installed in your environment.

  5. Understand blocking vs non-blocking behavior

    main
    • HTTP/1.1: Accessing the Response.content property will block until the entire response has been downloaded.
    • HTTP/2+: If using a multiplexed connection, non-consumed responses (stream=True) will no longer block the connection. Niquests can leverage multiplexing to prevent the synchronous loop from blocking on I/O per request.
    • Async Support: For native asynchronous workflows, use niquests.AsyncSession, which provides the same API as niquests.Session but with asyncio support.
  6. How WebSockets work in Niquests

    main

    Niquests handles WebSockets by upgrading an HTTP connection. When you perform a .get() request using a ws:// or wss:// URL, the resulting Response object contains a .extension attribute that provides the WebSocket interface.

    Important Notes:

    • If the server denies the WebSocket upgrade during the establishment phase, resp.extension will be None.
    • A successful WebSocket upgrade typically returns a status code of 101 (Switching Protocols).
    • Niquests supports WebSocket over HTTP/1.1 by default, but can support HTTP/2 and HTTP/3 (RFC8441) if you use the specific URL scheme wss+rfc8441://.
  7. Use Session objects to persist parameters and cookies

    main

    A Session (or AsyncSession) object allows you to persist parameters, authentication, and cookies across multiple requests. It also enables connection pooling via urllib3.future, which improves performance by reusing underlying TCP connections to the same host.

    Cookies received during a request in a session are automatically stored and sent in subsequent requests made with that same session instance.

    Default Parameters

    You can set default values for headers, authentication, and other parameters on the session object. These will be applied to every request made by that session. If a request method provides its own value for a parameter, it will override the session-level default.

    Merging and Overriding

    • Merging: Dictionaries passed to request methods (like headers or cookies) are merged with session-level values.
    • Overriding: Method-level parameters override session parameters.
    • Omission: To omit a specific key that exists at the session level from a single request, set its value to None in the method-level parameter.
    • Non-persistence: Parameters passed directly to a request method (e.g., s.get(..., cookies={...})) are not persisted to the session for future requests.
    # Sync example
    s = niquests.Session()
    s.auth = ('user', 'pass')
    s.headers.update({'x-test': 'true'})
    
    # 'x-test' (from session) and 'x-test2' (from method) are both sent
    s.get('https://httpbin.org/headers', headers={'x-test2': 'true'})
  8. Browser Sandbox limitations in Pyodide

    main

    When running under Pyodide, the browser's network stack handles connections, which imposes several limitations due to the browser sandbox:

    • DNS: Handled by the browser. Custom resolvers, DNS-over-HTTPS, and Protocol toggles are ignored.
    • TLS: Handled by the browser. verify and cert parameters are ignored; the browser uses its own certificate store. response.conn_info is unset.
    • CORS: Applies. The remote server must include appropriate Access-Control-Allow-Origin headers.
    • HTTP Version: response.http_version is None as the browser does not easily expose the negotiated protocol.
    • Forbidden Headers: The browser forbids overriding headers like Host, Origin, Cookie, and Connection.
    • Sockets: No HTTP+Unix sockets available.
    • Connection Management: Pool sizing, HTTP version toggles, and multiplexing settings are ignored; the browser manages the pool.
    • Redirection: No redirection history is accessible; disable autoredirect is not supported.
    • Proxies: Not allowed/accessible via WASM/JS.
    • Hooks & Extras: pre_send and early_response hooks are ignored. Extras like socks, ocsp, speedups, and zstd are not used.
  9. Understand Response encoding behavior

    main

    Niquests attempts to guess the encoding for Response.text using the following priority:

    1. The encoding specified in the HTTP headers.
    2. If no header is present or it is invalid, charset_normalizer is used to guess the encoding.

    Important Behavior Changes:

    • If Niquests cannot determine a suitable encoding, Response.text will return None. This is a security and stability measure to avoid accidental decoding of large binary payloads or non-strict decoding of invalid payloads.
    • To bypass guessing, you can manually set Response.encoding or access the raw bytes via Response.content.
  10. Use multiplexing and s.gather() in AsyncSession

    main

    When enabling multiplexed=True in an AsyncSession, you must call await s.gather() to avoid blocking the event loop and to ensure responses are fully processed before accessing them.

    Warning: Accessing non-awaitable attributes or methods of a lazy AsyncResponse without first calling s.gather() will raise an error.

    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())
  11. Create stateful middleware using Class-Based Hooks

    main

    Niquests supports class-based hooks via LifeCycleHook (synchronous) and AsyncLifeCycleHook (asynchronous). Unlike dictionary-based hooks, these allow for persistent state, better organization, and easier typing. You can implement specific lifecycle events by overriding methods such as pre_request, pre_send, on_upload, early_response, or response.

    Warning: In synchronous multi-threaded mode, you are responsible for ensuring thread safety (e.g., via locking) if your LifeCycleHook shares state between threads.

    import asyncio
    from niquests import AsyncSession, AsyncLifeCycleHook
    
    class ConnectionLogger(AsyncLifeCycleHook):
        async def pre_send(self, prepared_request, **kwargs) -> None:
            # Inspect the connection info before the request is sent
            print(f"Connected to: {prepared_request.conn_info}")
    
    async def main():
        async with AsyncSession() as s:
            # Pass an instance of your hook class
            await s.get("https://one.one.one.one", hooks=ConnectionLogger())
    
    if __name__ == "__main__":
        asyncio.run(main())