Skip to content

feat: make HTTP/3 opt-in in the Rust crate - #595

Merged
barjin merged 1 commit into
masterfrom
feat/http3-opt-in
Oct 26, 2026
Merged

barjin merged 1 commit into
masterfrom
feat/http3-opt-in

Conversation

@metalwarrior665

@metalwarrior665 metalwarrior665 commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Found out about this during profiling my Rust Actors based on crawlee-rs. The feature in cargo.toml is causing this in reqwest

TlsBackend::BuiltRustls(conn) => {
    #[cfg(feature = "http3")]
    {
        let mut h3_tls = conn.clone();
        h3_tls.alpn_protocols = vec!["h3".into()];
        h3_connector = build_h3_connector(resolver.clone(), h3_tls, ...)?;
    }

AI Summary

reqwest's http3 feature makes every client that uses use_preconfigured_tls build an HTTP/3 connector, even when HTTP/3 is never used. That means a quinn endpoint per client: a UDP socket, a driver task, and a zeroed receive buffer of at least 2 MB.

In a CPU profile of a Rust Instagram scraper that builds one impit client per proxy session, this endpoint setup took ~6% of all CPU, for a protocol it never uses.

HTTP/3 support is now behind a new http3 feature of the impit crate:

  • With the feature off, clients skip the QUIC setup, and hickory-* and tokio aren't needed.
  • The crate builds without --cfg reqwest_unstable.
  • impit-node and impit-python enable the feature, so their http3 option behaves as before.
  • For Rust users, with_http3() now needs features = ["http3"]. It's removed from the README and doc examples.

CI now also runs cargo test -p impit --features http3, and the docs are built with the feature on.

The proxy fix that used to be part of this PR is now #598. Whichever of the two lands second needs #[cfg(feature = "http3")] on #598's test, which calls with_http3().

🤖 Generated with Claude Code

https://claude.ai/code/session_0148RYR4BTGyCYHENSRZjC3S

reqwest's `http3` feature makes every client set up a QUIC endpoint (a UDP
socket, a driver task and a receive buffer of at least 2 MB), even when HTTP/3
is never used. HTTP/3 support is now behind the `http3` feature of the `impit`
crate, so clients without it skip that setup, and the crate builds without
`--cfg reqwest_unstable`. The Node and Python bindings enable the feature, so
their `http3` option works as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148RYR4BTGyCYHENSRZjC3S
@metalwarrior665 metalwarrior665 changed the title fix: never send HTTP/3 requests past the proxy, make HTTP/3 opt-in in the Rust crate feat: make HTTP/3 opt-in in the Rust crate Oct 23, 2026
@metalwarrior665
metalwarrior665 requested a balanced review from Copilot October 23, 2026 21:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The feature gates, binding configuration, documentation, and CI changes are consistent, with no blocking issues identified.

No open issues 🎉

What changed in this PR

Makes HTTP/3 opt-in for the Rust crate, avoiding unnecessary QUIC setup for default clients while preserving HTTP/3 support in the Node.js and Python bindings.

Changes:

  • Adds an http3 Cargo feature and gates HTTP/3-specific code and dependencies.
  • Updates examples and documentation to reflect the new opt-in requirement.
  • Adds feature-enabled CI tests and includes HTTP/3 APIs in generated documentation.
File Description
README.md Documents HTTP/3 opt-in and simplifies the example.
impit/​src/​lib.rs Gates the HTTP/3 module and updates crate documentation.
impit/​src/​impit.rs Gates HTTP/3 functionality and tests default-disabled behavior.
impit/​README.md Updates feature requirements and the example.
impit/​examples/​basic.rs Removes HTTP/3 from the default example.
impit/​Cargo.toml Defines the feature and makes supporting dependencies optional.
impit-python/​Cargo.toml Enables HTTP/3 for Python bindings.
impit-node/​Cargo.toml Enables HTTP/3 for Node.js bindings.
.github/​workflows/​format.yaml Adds tests with HTTP/3 enabled.
.github/​workflows/​docs.yaml Builds documentation with HTTP/3 enabled.

Review effort: Balanced

@metalwarrior665
metalwarrior665 marked this pull request as ready for review October 24, 2026 08:09

@barjin barjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm, thank you @metalwarrior665 !

@barjin
barjin merged commit 2d2b5e4 into master Oct 26, 2026
64 of 66 checks passed
@barjin
barjin deleted the feat/http3-opt-in branch October 26, 2026 08:53
barjin added a commit that referenced this pull request Oct 26, 2026
My agent got freaked by the weird http3 + proxy behavior. This is an
attempt to clean it up so it returns an error and doesn't silently
change behavior.

## AI Summary

HTTP/3 through a proxy isn't supported, and the docs say so. That was
decided in #246 and #364, documented in #373, and reqwest tracks it
upstream as seanmonstar/reqwest#2833. This PR doesn't try to add that
support. It makes the unsupported combination fail safe instead of
quietly sending traffic from the machine's own IP address.

The change only matters when **HTTP/3 is enabled and a proxy is set**,
whether through `with_proxy` / `proxyUrl` or through `HTTPS_PROXY` /
`ALL_PROXY`. Everything else behaves as before, including the default,
where HTTP/3 is off.

## What `master` does with HTTP/3 and a proxy

| Request | On `master` |
|---|---|
| `http3_prior_knowledge` | Every request goes straight to the target
over QUIC from the real IP. The proxy is never used. |
| Not forced, and the host advertises h3 in its DNS HTTPS record |
`H3Engine` looks the record up directly at `8.8.8.8`, which already
exposes the hostname from the real IP. The request then goes out over
QUIC from the real IP too. |
| Not forced, and the host advertises h3 only through `Alt-Svc` | The
first request goes through the proxy. impit then caches the host as
h3-capable, and every later request to it leaks. A quick one-request
test looks fine. |

None of these cases logs anything or returns an error.

I reproduced this on `master` with a local logging CONNECT proxy and
`https://www.google.com/`. The forced request never reached the proxy.
strace showed QUIC packets going directly to `142.251.153.119:443`, and
the DNS lookups going to `8.8.8.8` / `8.8.4.4`.

## With this PR

- When the client is built with a proxy, HTTP/3 is disabled and a
warning is logged. All traffic goes through the proxy over TCP (HTTP/2
or HTTP/1.1), and no lookups go directly to `8.8.8.8`.
- A request with `http3_prior_knowledge` fails with `Http3Disabled`,
whose message now mentions the proxy. It no longer goes out directly.
- A proxy that comes only from `HTTPS_PROXY` / `ALL_PROXY` is covered
too. On `master` it leaks the same way, and the docs don't mention it.

The PR also removes `.with_http3()` from the `ImpitBuilder` doc example,
which combined it with a proxy.

## Not changed

- HTTP/3 still can't go through a proxy. That's blocked in reqwest.
- `NO_PROXY` isn't considered. If any proxy is set, HTTP/3 is off for
all hosts. That's a deliberately conservative choice.
- I disable HTTP/3 rather than fail at build time, so existing `http3:
true` + proxy setups keep working over HTTP/2. That's easy to flip to an
error if you'd prefer.
- The unused QUIC endpoint that reqwest sets up whenever the `http3`
feature is compiled in is handled separately in #595.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0148RYR4BTGyCYHENSRZjC3S

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jindřich Bär <jindrichbar@gmail.com>
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.

5 participants