Skip to content

bitreq: add SOCKS5 proxy support#533

Open
FreeOnlineUser wants to merge 1 commit into
rust-bitcoin:masterfrom
FreeOnlineUser:socks5-proxy
Open

bitreq: add SOCKS5 proxy support#533
FreeOnlineUser wants to merge 1 commit into
rust-bitcoin:masterfrom
FreeOnlineUser:socks5-proxy

Conversation

@FreeOnlineUser
Copy link
Copy Markdown

@FreeOnlineUser FreeOnlineUser commented Mar 20, 2026

Add SOCKS5 proxy support (RFC 1928) alongside the existing HTTP CONNECT path.

Motivation

ldk-node needs to route HTTP calls (RGS, scoring, LNURL-auth) through Tor's SOCKS proxy when TorConfig is enabled. The existing proxy feature only supports HTTP CONNECT, which doesn't handle .onion addresses. See lightningdevkit/ldk-node#834.

SOCKS5 and HTTP CONNECT serve different use cases and now coexist behind the same proxy feature flag:

Public API

  • Proxy::new_socks5(addr): unauthenticated SOCKS5. Accepts both socks5:// and socks5h:// URL schemes (identical behaviour, since destinations are always forwarded as ATYP 0x03 hostnames).
  • Proxy::new_socks5_with_credentials(addr, user, pass): RFC 1929 username/password auth at construction. Tor uses these credentials for circuit isolation.
  • Proxy::set_credentials(user, pass): mutate credentials on an existing Proxy. Lets a caller hold one Proxy and rotate credentials between connections to obtain fresh Tor circuits, without rebuilding from a URL.

Implementation

  • New ProxyKind::Socks5 variant.
  • Sync and async handshake paths share protocol logic via 5 extracted helpers (greeting, auth, connect framing). Only the I/O calls differ between sync and async.
  • Connection layer branches on proxy.is_socks5() before the existing HTTP CONNECT path.

Tests

18 SOCKS5-specific tests:

  • 10 parsing / credential: host+port parsing, socks5:// and socks5h:// schemes, default port (1080), wrong-protocol rejection, is_socks5 check, credential constructor, RFC 1929 length validation (constructor and setter), credential rotation.
  • 8 mock handshake: success, .onion domain, server rejection, port encoding (big-endian), domain >255 bytes, RFC 1929 credential auth, credential rejection, no-auth fallback.

Unit tests, doctests, and clippy are all clean.

Notes


Assisted by Joe (Claude Opus 4.7 under the hood).

Copy link
Copy Markdown
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Took a first very high-level look.

Comment thread bitreq/src/connection.rs Outdated
}
Some(proxy) => {
// do proxy things
// HTTP CONNECT proxy
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should be good to just drop the HTTP proxy behavior? Not sure anybody is going to use that, and it might make things simpler?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

HTTP CONNECT is what payjoin originally requested in #472, so I'd keep it. They serve different use cases: SOCKS5 for Tor/general proxying, HTTP CONNECT for corporate forward proxies and HTTPS tunneling. Happy to refactor if the maintainers agree it should go, but wanted to flag the other stakeholders first.

Comment thread bitreq/src/proxy.rs
/// let request = bitreq::post("http://example.com").with_proxy(proxy);
/// ```
///
pub fn new_socks5<S: AsRef<str>>(proxy: S) -> Result<Self, Error> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note that Tor (mis-)uses user credentials to trigger circuit rebuilding/stream isolation. So we probably want to allow for an interface that at least lets the user refresh/change the credentials freely on the Proxy object, so that subsequent connections are using a new circuit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good call. I'll add username/password support (RFC 1929 sub-negotiation) to the SOCKS5 handshake. When credentials are set, the greeting advertises method 0x02 instead of 0x00, and Tor uses the unique credentials to isolate circuits. The Proxy API can accept optional credentials at construction or via a setter for rotation.

@FreeOnlineUser
Copy link
Copy Markdown
Author

Updated: added Proxy::new_socks5_with_credentials for RFC 1929 auth (Tor circuit isolation), credential length validation, extracted shared protocol helpers to deduplicate sync/async, 15 tests (was 10).

@tnull
Copy link
Copy Markdown
Collaborator

tnull commented Apr 24, 2026

@FreeOnlineUser any update on this? Is this ready to take out of draft?

@daywalker90
Copy link
Copy Markdown

FYI: https://github.com/rust-bitcoin/bitcoin-payment-instructions needs socks5h support for remote DNS lookups.

@FreeOnlineUser FreeOnlineUser marked this pull request as ready for review April 25, 2026 08:11
@FreeOnlineUser
Copy link
Copy Markdown
Author

@tnull yes, ready. Apologies for the radio silence. Pulling threads together:

HTTP CONNECT path (your March 20 question on dropping it): kept it. payjoin in #472 was the original requester for proxy support and they use HTTP CONNECT. Happy to drop in a follow-up PR if you'd rather, but it's separable from this work and doesn't block SOCKS5 going green.

Credential refresh interface (your March 20 ask for rotating credentials freely): added Proxy::set_credentials(user, pass). A caller can now hold one long-lived Proxy and rotate credentials per connection for Tor circuit isolation without rebuilding from a URL. Tested with a rotation case. Open to also adding a chained with_credentials(self, ...) builder if you'd prefer that shape too.

socks5h scheme (@daywalker90's hint about bitcoin-payment-instructions needing remote DNS): we already always do remote DNS at the proxy via ATYP 0x03 (DOMAIN). I've added socks5h:// as an accepted URL alias to match curl convention. Both schemes behave identically and the doc explicitly states this.

Hang risk on the binary handshake: this PR's read-timeout behaviour matches the existing HTTP CONNECT path, which also doesn't set per-read deadlines on the raw stream during proxy negotiation. Improving this for both paths is left as a follow-up so we don't expand scope here.

The description has been refreshed to reflect everything currently shipped (RFC 1929 credentials, socks5h://, set_credentials, 18 SOCKS5-specific tests). Out of draft.

Comment thread bitreq/src/connection.rs
Some(proxy) => {
// do proxy things
// HTTP CONNECT proxy
let mut tcp = Self::tcp_connect(&proxy.server, proxy.port, timeout_at)?;
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.

Mind refactoring this code so that it just always calls handshake... and doesn't care what type it is and that's abstracted by the proxy?

Comment thread bitreq/src/proxy.rs Outdated
0x03 => { // Domain: 1 len byte + domain + 2 port
let mut len = [0u8; 1];
stream.read_exact(&mut len).map_err(Error::IoError)?;
let mut buf = vec![0u8; len[0] as usize + 2];
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.

please dont allocate to throw away <= 258 bytes.

Comment thread bitreq/src/proxy.rs

/// Perform a SOCKS5 handshake on a connected TCP stream (sync).
#[cfg(feature = "std")]
pub(crate) fn socks5_handshake_sync(
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.

Can we DRY the handshake method? Maybe with a simple macro generating both the async and sync versions?

@FreeOnlineUser FreeOnlineUser force-pushed the socks5-proxy branch 2 times, most recently from 055a0b8 to 8cfda31 Compare April 30, 2026 09:01
@FreeOnlineUser
Copy link
Copy Markdown
Author

@TheBlueMatt thanks for the review. All three addressed in 8cfda31.

Allocation (proxy.rs:329): Switched the domain-ATYP arm to [u8; 257] with a slice. Both sync and async paths fixed; matches the IPv4 / IPv6 arms in the same match.

Polymorphism (connection.rs): Added Proxy::handshake_sync / Proxy::handshake_async pub(crate) dispatchers, with private http_connect_handshake_sync / _async methods owning the moved CONNECT logic. The connection layer now treats every proxy uniformly:

Some(proxy) => {
    let mut tcp = Self::tcp_connect(&proxy.server, proxy.port, timeout_at)?;
    proxy.handshake_sync(&mut tcp, params.host, params.port)?;
    Ok(tcp)
}

The is_socks5() predicate has no remaining lib callers and was dropped. Added two mock-server tests for the HTTP CONNECT path through the new dispatcher.

Sync/async DRY (proxy.rs:281): Done with two macro_rules! macros (socks5_handshake_body! and http_connect_handshake_body!) at module scope. Each takes the call-site idents (self, stream, target_host, target_port) and a token-tree $($maybe_await:tt)* that's empty for sync and .await for async, inserted at every I/O call. Macro hygiene means the parameter list is explicit, but the body is single-source.

The four handshake methods now look like:

pub(crate) fn socks5_handshake_sync(
    &self, stream: &mut std::net::TcpStream,
    target_host: &str, target_port: u16,
) -> Result<(), Error> {
    use std::io::{Read, Write};
    socks5_handshake_body!(self, stream, target_host, target_port;)
}

Net effect: -59 lines in proxy.rs versus the previous round, and any future protocol change happens in one place.

Also picked up a dead tokio::io::AsyncReadExt import that fell out of the connection.rs rewrite.

Comment thread bitreq/src/proxy.rs
Copy link
Copy Markdown
Member

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Sorry for the delay, a few nits and one bug.

Comment thread bitreq/src/proxy.rs Outdated
/// Macro hygiene means the call site must pass `self`, `stream`, and the
/// target host/port explicitly.
macro_rules! socks5_handshake_body {
($self:ident, $stream:ident, $target_host:ident, $target_port:ident; $($maybe_await:tt)*) => {{
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.

nit: I believe this would be more correct with a ? not a * (which would ripple through the whole macro).

Suggested change
($self:ident, $stream:ident, $target_host:ident, $target_port:ident; $($maybe_await:tt)*) => {{
($self:ident, $stream:ident, $target_host:ident, $target_port:ident; $($maybe_await:tt)?) => {{

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

There's already a precedent for this in bitreq: response.rs defines a maybe_await! helper used by define_read_methods! to thread .await through a sync/async-templated body, with the outer macro taking the keyword via $(, $await: tt)?. Refactored both socks5_handshake_body! and http_connect_handshake_body! to use the same shape:

macro_rules! socks5_handshake_body {
    ($self:ident, $stream:ident, $target_host:ident, $target_port:ident $(, $await:tt)?) => {{
        ...
        maybe_await!($stream.write_all(&greeting), $($await)?).map_err(Error::IoError)?;
        ...
    }};
}

// sync:  socks5_handshake_body!(self, stream, target_host, target_port)
// async: socks5_handshake_body!(self, stream, target_host, target_port, await)

Aligns with the existing define_read_methods! convention. Pushed in 224343b.

Comment thread bitreq/src/proxy.rs Outdated

let mut greeting_resp = [0u8; 2];
$stream.read_exact(&mut greeting_resp) $($maybe_await)*.map_err(Error::IoError)?;
Self::socks5_check_greeting(&greeting_resp, expected_method)?;
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.

Doesn't really seem worth stubbing out to a function to check two bytes and return an error?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Inlined both 2-byte helpers (socks5_check_greeting, socks5_check_auth) into the handshake macro body and removed the standalone fns. Pushed in 224343b.

Comment thread bitreq/src/proxy.rs Outdated
return Err(Error::ProxyConnect);
}
if n < buf.len() {
// Partial read indicates end of response.
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.

No, it might just indicate we received one TCP packet and are waiting on more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Worth raising the bigger question: should HTTP CONNECT come out entirely?

A few reasons:

  • Making this read loop correct isn't trivial (Content-Length, chunked encoding, or read-until-close, each with their own edge cases).
  • tnull floated removal back in March. I argued to keep it for payjoin-style HTTP-only callers at the time, but I don't have a concrete use case (ldk-node Tor is SOCKS5-only).
  • Removing it deletes the second macro_rules! block, so the abstraction question on the SOCKS5 macro stands on its own.

I'm happy to either fix the response-read here or strip HTTP CONNECT in this PR. Slight lean toward removal. Let me know which way you'd prefer.

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.

That's a fair question, I wouldn't be too against removing it, but we do also actually have HTTP response-parsing in this crate....maybe we should find a way to use the Response type we already have to read the headers so that the code here can be ~trivial?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Took the latter path. Made parse_status_line pub(crate) and rewrote the handshake to read into an [u8; 8192] stack buffer until \r\n\r\n, then parse the status line through the shared helper:

let mut buf = [0u8; 8192];
let mut len = 0;
let header_end = loop {
    let n = maybe_await!($stream.read(&mut buf[len..]), $($await)?).map_err(Error::IoError)?;
    if n == 0 { return Err(Error::ProxyConnect); }
    len += n;
    if let Some(idx) = buf[..len].windows(4).position(|w| w == b"\r\n\r\n") {
        break idx;
    }
    if len == buf.len() { return Err(Error::ProxyConnect); }
};

let headers = core::str::from_utf8(&buf[..header_end]).map_err(|_| Error::ProxyConnect)?;
let status_line = headers.lines().next().ok_or(Error::ProxyConnect)?;
let (status_code, _) = crate::response::parse_status_line(status_line);

Drops verify_response (the partial-read-as-EOF bug goes with it) and the heap Vec. Added a handshake_handles_split_response test that writes the response in two TCP segments to exercise the loop. Pushed in 69b50bc.

Comment thread bitreq/src/proxy.rs Outdated
}

/// Build the SOCKS5 CONNECT request for a domain target.
fn socks5_connect_request(target_host: &str, target_port: u16) -> Result<Vec<u8>, Error> {
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.

Please do not allocate a vec for < 8192 bytes. Same goes for socks5_auth_request.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Switched both to fixed-size stack buffers:

  • socks5_auth_request([u8; 513], usize) (max 3 + 255 + 255)
  • socks5_connect_request([u8; 262], usize) (max 4 + 1 + 255 + 2)

Sliced with the length at the call site. Pushed in 69b50bc.

Comment thread bitreq/src/proxy.rs Outdated
return Err(Error::ProxyConnect);
}
if n < buf.len() {
// Partial read indicates end of response.
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.

That's a fair question, I wouldn't be too against removing it, but we do also actually have HTTP response-parsing in this crate....maybe we should find a way to use the Response type we already have to read the headers so that the code here can be ~trivial?

@FreeOnlineUser FreeOnlineUser force-pushed the socks5-proxy branch 2 times, most recently from e633c9b to 69b50bc Compare May 19, 2026 02:43
Copy link
Copy Markdown
Member

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

A few last comments, otherwise lgtm.

Comment thread bitreq/src/proxy.rs Outdated
Ok(p)
}

/// Sets RFC 1929 username/password credentials on this proxy.
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.

This also applies to HTTP proxies, though, so calling out the RFC number is wrong. It should ideally allow for longer user/password in HTTP mode and the docs should be clearer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. Dropped the RFC 1929 framing from the opener and split the length-limit guidance by proxy kind: SOCKS5 enforces 1-255 / 0-255 (RFC 1929 wire-format), HTTP Basic (RFC 7617) has no protocol-level limit. New test http_set_credentials_no_length_limit exercises the HTTP path with 1024-byte credentials.

Comment thread bitreq/src/proxy.rs Outdated
/// // ... later, for a fresh circuit:
/// proxy.set_credentials("session-2", "x").unwrap();
/// ```
pub fn set_credentials(&mut self, user: &str, password: &str) -> Result<(), Error> {
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.

If its gonna make them a String, it should take them as a String, not &str.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. set_credentials now takes String; new_socks5_with_credentials cascades the same change so the constructor and the setter agree. Callers can hand over an existing allocation. Existing tests and the doc example updated.

Comment thread bitreq/src/proxy.rs
/// Build the SOCKS5 greeting bytes.
/// Returns (greeting_bytes, expected_auth_method).
fn socks5_greeting(&self) -> ([u8; 3], u8) {
let method = if self.user.is_some() { 0x02 } else { 0x00 };
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.

what about empty-user-some-password?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Rejected, matching RFC 1929 ULEN >= 1. The existing validation test covered ("", "pass") but didn't name the empty-user-non-empty-password case explicitly; added that assertion next to it (and the equivalent constructor case). Happy to relax if you'd prefer Tor-style opaque credentials where ULEN=0 is acceptable.

Comment thread bitreq/src/proxy.rs Outdated
return Err(Error::ProxyConnect);
}
let mut buf = [0u8; 262];
buf[..5].copy_from_slice(&[0x05, 0x01, 0x00, 0x03, host_bytes.len() as u8]);
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.

Are we allowed to "encode" an IP by just writing it out as a "domain" here, or do we need to encode it properly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Spec answer: ATYP 0x03 with a stringified IP is technically allowed (the field is opaque bytes), but de facto standard is to detect and encode properly.

Prior art:

  • curl (lib/socks.c:780-891): tries inet_pton(AF_INET6, ...) then AF_INET, falls through to domain
  • Go x/net/internal/socks: net.ParseIP(host) then branches on nil/To4()/IPv6
  • sfackler/rust-socks and sticnarf/tokio-socks: encode the choice in the type system at API entry (TargetAddr::Ip | Domain enum + conversion trait)

Practical impact for our targets is low (Tor recognizes IP literals and doesn't DNS them), but conformance is the right default and it mirrors the response parser at lines 77-89, which already handles all three ATYPs.

Sketch:

let body_end = match target_host.parse::<IpAddr>() {
    Ok(IpAddr::V4(v4)) => {
        buf[3] = 0x01;
        buf[4..8].copy_from_slice(&v4.octets());
        8
    }
    Ok(IpAddr::V6(v6)) => {
        buf[3] = 0x04;
        buf[4..20].copy_from_slice(&v6.octets());
        20
    }
    Err(_) => {
        let host_bytes = target_host.as_bytes();
        if host_bytes.len() > 255 { return Err(Error::ProxyConnect); }
        buf[3] = 0x03;
        buf[4] = host_bytes.len() as u8;
        buf[5..5 + host_bytes.len()].copy_from_slice(host_bytes);
        5 + host_bytes.len()
    }
};

[u8; 262] buffer unchanged (domain max still wins). Existing test at line 624 asserts 0x03; would parametrize over the three ATYPs.

Happy to fold this into the same push as the other May 21 nits.

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.

Yea, if everyone else does it probably let's do it too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. target_host.parse::<IpAddr>() then dispatch on V4 / V6 / Err. Mirrors curl's inet_pton flow and matches the response parser above which reads all three ATYPs. Mock server updated to dispatch on the ATYP byte; new tests handshake_ipv4_literal_uses_atyp_one and handshake_ipv6_literal_uses_atyp_four cover both literal paths. Existing domain tests now assert ATYP=0x03 so we'd catch regression.

Add SOCKS5 proxy support (RFC 1928) alongside existing HTTP CONNECT.
Targets are encoded per RFC 1928: IPv4 literals as ATYP 0x01, IPv6
literals as ATYP 0x04, and anything else as a domain name (ATYP 0x03)
with DNS resolution left to the proxy. The latter enables .onion
routing through Tor.

New public API:
- `Proxy::new_socks5(addr)` for unauthenticated SOCKS5. Accepts both
  `socks5://` and `socks5h://` URL schemes; both behave identically.
- `Proxy::new_socks5_with_credentials(addr, user, pass)` for RFC 1929
  username/password auth at construction.
- `Proxy::set_credentials(user, pass)` to mutate credentials on an
  existing `Proxy`. Takes `String` so callers can hand over an
  existing allocation. Lets a caller hold one `Proxy` and rotate
  credentials for Tor circuit isolation without rebuilding from URL.
  Length validation is kind-aware: SOCKS5 enforces RFC 1929's 1-255 /
  0-255 byte limits; HTTP Basic (RFC 7617) has no protocol-level limit.

Internally, both proxy kinds expose a single `handshake_sync` /
`handshake_async` entry point. The connection layer treats every
proxy uniformly and no longer branches on type. Sync and async
handshake bodies share protocol logic via two `macro_rules!` macros
that template over `.await` insertion.

Motivation: ldk-node needs to route HTTP calls (RGS, scoring) through
Tor's SOCKS proxy. See lightningdevkit/ldk-node#834.

Tests cover SOCKS5 parsing and credentials (including empty-user and
length-limit edge cases per proxy kind), SOCKS5 mock-server handshakes
(success, .onion, server rejection, port encoding, domain length,
IPv4/IPv6 literal ATYP encoding, credential auth, rejection, no-auth,
rotation), and HTTP CONNECT mock-server handshakes through the
polymorphic dispatcher.
@FreeOnlineUser
Copy link
Copy Markdown
Author

Round 3 addresses the May 21 inline asks. Force-pushed 69b50bc → 3f2012c.

  • L271 (set_credentials doc framing): dropped the RFC 1929 wording from the opener; length-limit guidance now branches by proxy kind (SOCKS5 = RFC 1929 1-255 / 0-255; HTTP Basic = no protocol limit).
  • L288 (&strString): set_credentials and new_socks5_with_credentials now take String.
  • L301 (empty-user-some-password): rejected, matching RFC 1929 ULEN >= 1. Added an explicit named assertion next to the existing validation test.
  • L335 (IP ATYP encoding): target_host.parse::<IpAddr>() then dispatch on V4 / V6 / Err. New mock-server-driven tests for IPv4 and IPv6 literals; existing domain tests now assert ATYP=0x03 explicitly.

Open follow-up (your May 18 ask, not done here): reusing the Response type for HTTP CONNECT header parsing. Happy to do as a separate PR after this lands, or fold in if you'd prefer.

26 unit tests + 4 doc-tests passing locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants