• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 26173671454

20 May 2026 03:47PM UTC coverage: 84.348% (-0.9%) from 85.284%
26173671454

Pull #1514

github

web-flow
Merge b77bb7636 into 17a3b6889
Pull Request #1514: [WIP] Add AS-aware relay selection

551 of 814 new or added lines in 5 files covered. (67.69%)

7 existing lines in 2 files now uncovered.

12087 of 14330 relevant lines covered (84.35%)

378.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.2
/payjoin-cli/src/app/v2/ohttp.rs
1
//! OHTTP relay selection and key bootstrapping for the payjoin-cli.
2
//!
3
//! Bootstrap key fetching uses temporary relay failover. Protocol requests use
4
//! stateless relay selection from the receiver network selection.
5
use std::time::Duration;
6

7
use anyhow::{anyhow, Result};
8
use reqwest::header::ACCEPT;
9
use reqwest::Proxy;
10

11
use super::relay_selection::{ReceiverNetworkSelection, ResolvedUrl};
12
use super::Config;
13
use crate::app::http_client_builder;
14

15
#[derive(Debug)]
16
pub(crate) enum RelayAttemptError {
17
    /// Network-shaped failures can try the next relay candidate.
18
    Retryable(anyhow::Error),
19
    /// Protocol/configuration-shaped failures should stop immediately.
20
    Terminal(anyhow::Error),
21
}
22

23
/// Decide whether a reqwest failure should fail over to another relay.
NEW
24
pub(crate) fn classify_reqwest_error(
×
NEW
25
    err: reqwest::Error,
×
NEW
26
    context: &'static str,
×
NEW
27
) -> RelayAttemptError {
×
NEW
28
    let error = anyhow!("{context}: {err}");
×
NEW
29
    if err.is_timeout() || err.is_connect() || err.is_request() {
×
NEW
30
        RelayAttemptError::Retryable(error)
×
31
    } else {
NEW
32
        RelayAttemptError::Terminal(error)
×
33
    }
UNCOV
34
}
×
35

36
pub(crate) async fn unwrap_ohttp_keys_or_else_fetch(
2✔
37
    config: &Config,
2✔
38
    network_selection: &ReceiverNetworkSelection,
2✔
39
) -> Result<payjoin::OhttpKeys> {
2✔
40
    if let Some(ohttp_keys) = config.v2()?.ohttp_keys.clone() {
2✔
41
        println!("Using OHTTP Keys from config");
2✔
42
        Ok(ohttp_keys)
2✔
43
    } else {
UNCOV
44
        println!("Bootstrapping private network transport over Oblivious HTTP");
×
NEW
45
        fetch_ohttp_keys(config, network_selection).await
×
46
    }
47
}
2✔
48

49
// Fetch directory OHTTP keys through the already chosen receiver network selection.
50
// This happens before the receiver key exists, so it cannot use RelaySelector.
UNCOV
51
async fn fetch_ohttp_keys(
×
UNCOV
52
    config: &Config,
×
NEW
53
    network_selection: &ReceiverNetworkSelection,
×
NEW
54
) -> Result<payjoin::OhttpKeys> {
×
NEW
55
    if network_selection.relays.is_empty() {
×
NEW
56
        return Err(anyhow!(
×
NEW
57
            "No valid relays available for {}",
×
NEW
58
            network_selection.directory.url.as_str()
×
NEW
59
        ));
×
NEW
60
    }
×
61

NEW
62
    let last_relay_index = network_selection.relays.len() - 1;
×
NEW
63
    for (index, relay) in network_selection.relays.iter().enumerate() {
×
NEW
64
        match fetch_directory_ohttp_keys_via_resolved_relay_url(
×
NEW
65
            config,
×
NEW
66
            &relay.resolved,
×
NEW
67
            &network_selection.directory,
×
68
        )
NEW
69
        .await
×
70
        {
NEW
71
            Ok(keys) => return Ok(keys),
×
NEW
72
            Err(RelayAttemptError::Retryable(error)) => {
×
NEW
73
                tracing::debug!(
×
74
                    "Failed to fetch OHTTP keys via relay {}: {error:?}",
75
                    relay.resolved.url
76
                );
NEW
77
                if index == last_relay_index {
×
NEW
78
                    return Err(error);
×
NEW
79
                }
×
80
            }
NEW
81
            Err(RelayAttemptError::Terminal(error)) => return Err(error),
×
82
        }
83
    }
84

NEW
85
    unreachable!(
×
86
        "empty relay selections return before the loop and successful key fetches return inside it"
87
    )
NEW
88
}
×
89

90
// This mirrors payjoin::io::fetch_ohttp_keys, but keeps the CLI-specific
91
// pieces: resolved socket addresses, configured TLS roots, and relay failover
92
// error classification.
93
// Build a proxied request through one resolved relay. The relay address is
94
// resolved to the DNS result checked by relay_selection.
NEW
95
async fn fetch_directory_ohttp_keys_via_resolved_relay_url(
×
NEW
96
    config: &Config,
×
NEW
97
    relay: &ResolvedUrl,
×
NEW
98
    directory: &ResolvedUrl,
×
NEW
99
) -> std::result::Result<payjoin::OhttpKeys, RelayAttemptError> {
×
NEW
100
    let proxy = Proxy::all(relay.url.as_str()).map_err(|err| {
×
NEW
101
        RelayAttemptError::Terminal(anyhow!("Failed to configure OHTTP relay proxy: {err}"))
×
NEW
102
    })?;
×
NEW
103
    let mut builder = http_client_builder(config)
×
NEW
104
        .map_err(|err| RelayAttemptError::Terminal(anyhow!("Failed to build HTTP client: {err}")))?
×
NEW
105
        .proxy(proxy);
×
106

NEW
107
    if let Some(domain) = relay.domain() {
×
NEW
108
        builder = builder.resolve_to_addrs(domain, &relay.socket_addrs);
×
NEW
109
    }
×
110

NEW
111
    if let Some(directory_domain) = directory.domain() {
×
NEW
112
        builder = builder.resolve_to_addrs(directory_domain, &directory.socket_addrs);
×
NEW
113
    }
×
114

NEW
115
    let client = builder.build().map_err(|err| {
×
NEW
116
        RelayAttemptError::Terminal(anyhow!("Failed to build HTTP client: {err}"))
×
NEW
117
    })?;
×
NEW
118
    let ohttp_keys_url = directory.url.join("/.well-known/ohttp-gateway").map_err(|err| {
×
NEW
119
        RelayAttemptError::Terminal(anyhow!("Failed to construct OHTTP key URL: {err}"))
×
NEW
120
    })?;
×
NEW
121
    let response = client
×
NEW
122
        .get(ohttp_keys_url.as_str())
×
NEW
123
        .timeout(Duration::from_secs(10))
×
NEW
124
        .header(ACCEPT, "application/ohttp-keys")
×
NEW
125
        .send()
×
NEW
126
        .await
×
NEW
127
        .map_err(|err| classify_reqwest_error(err, "Failed to fetch OHTTP keys"))?;
×
128

NEW
129
    if !response.status().is_success() {
×
NEW
130
        return Err(RelayAttemptError::Terminal(anyhow!(
×
NEW
131
            "Unexpected OHTTP key status code {}",
×
NEW
132
            response.status()
×
NEW
133
        )));
×
UNCOV
134
    }
×
135

NEW
136
    let body = response.bytes().await.map_err(|err| {
×
NEW
137
        RelayAttemptError::Terminal(anyhow!("Failed to read OHTTP key response body: {err}"))
×
NEW
138
    })?;
×
NEW
139
    payjoin::OhttpKeys::decode(&body)
×
NEW
140
        .map_err(|err| RelayAttemptError::Terminal(anyhow!("Failed to decode OHTTP keys: {err}")))
×
UNCOV
141
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc