Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions crates/utils/src/esplora_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,36 +165,36 @@ impl EsploraClient {

pub async fn get_merkleblock_proof(&self, txid: &Txid) -> Result<MerkleBlock> {
Ok(consensus::deserialize(&Vec::<u8>::from_hex(
&self.get(&format!("tx/{txid}/merkleblock-proof")).await?,
&self.get(&format!("/tx/{txid}/merkleblock-proof")).await?,
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This change introduces a significant issue for users who run Esplora behind a reverse proxy with a path prefix.

By adding a leading slash, you are making the path an "absolute-path reference". According to the url::Url::join documentation, this resolves the path from the origin of the base URL, ignoring any path component in the base URL.

For example, if a user configures the client with a base URL like https://example.com/esplora/ (note the trailing slash, which is correct for path prefixes), the behavior changes as follows:

  • Before (correct): url.join("tx/...") would result in https://example.com/esplora/tx/....
  • After (incorrect): url.join("/tx/...") would result in https://example.com/tx/....

The issue described in the pull request description ("the last segment of the base URL was being replaced") happens when the base URL has a path but lacks a trailing slash (e.g., https://example.com/esplora). The correct way to handle this is not to make all paths absolute, but to ensure the base URL is correctly formed.

I recommend a more robust solution:

  1. Revert this change and all similar changes in this PR.
  2. In EsploraClient::new_with_url, ensure that the parsed Url always has a path ending with a trailing slash. This makes path joining predictable. You could do something like this:
    pub fn new_with_url(esplora_url: String) -> Result<Self> {
        let mut url = esplora_url.parse::<Url>()?;
        if !url.path().ends_with('/') {
            url.path_segments_mut()
                .map_err(|_| eyre::eyre!("URL cannot be a base"))?
                .push("");
        }
        Ok(Self { url, cli: Client::new() })
    }
  3. Audit all other API calls in this file and ensure they all use relative paths (no leading slash) for consistency. For example, get_tx_hex and get_tx_info currently use absolute paths and should be changed.

This approach will correctly handle all base URL configurations, including those with path prefixes.

Suggested change
&self.get(&format!("/tx/{txid}/merkleblock-proof")).await?,
&self.get(&format!("tx/{txid}/merkleblock-proof")).await?,

Copy link
Contributor

Choose a reason for hiding this comment

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

@mdqst please check this comment

Copy link
Author

Choose a reason for hiding this comment

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

sry, my bad. closed.

)?)?)
}

pub async fn get_merkle_proof(&self, txid: &Txid) -> Result<MerkleProof> {
self.get_and_decode(&format!("tx/{txid}/merkle-proof")).await
self.get_and_decode(&format!("/tx/{txid}/merkle-proof")).await
}

pub async fn get_transactions_by_scripthash(
&self,
scripthash: &str,
) -> Result<Vec<TransactionFormat>> {
self.get_and_decode(&format!("scripthash/{scripthash}/txs")).await
self.get_and_decode(&format!("/scripthash/{scripthash}/txs")).await
}

pub async fn get_block_hash(&self, height: u32) -> Result<BlockHash> {
let response = self.get(&format!("block-height/{height}")).await?;
let response = self.get(&format!("/block-height/{height}")).await?;
Ok(BlockHash::from_str(&response)?)
}

pub async fn get_raw_block_header(&self, hash: &BlockHash) -> Result<Vec<u8>> {
Ok(Vec::<u8>::from_hex(&self.get(&format!("block/{hash}/header")).await?)?)
Ok(Vec::<u8>::from_hex(&self.get(&format!("/block/{hash}/header")).await?)?)
}

pub async fn get_block_header(&self, hash: &BlockHash) -> Result<Header> {
Ok(consensus::deserialize(&self.get_raw_block_header(hash).await?)?)
}

pub async fn get_block_value(&self, hash: &BlockHash) -> Result<BlockValue> {
self.get_and_decode(&format!("block/{hash}")).await
self.get_and_decode(&format!("/block/{hash}")).await
}

pub async fn get_raw_block_header_at_height(&self, height: u32) -> Result<Vec<u8>> {
Expand All @@ -206,15 +206,15 @@ impl EsploraClient {
}

pub async fn get_block_txids(&self, hash: &BlockHash) -> Result<Vec<Txid>> {
self.get_and_decode(&format!("block/{hash}/txids")).await
self.get_and_decode(&format!("/block/{hash}/txids")).await
}

pub async fn get_chain_height(&self) -> Result<u32> {
Ok(self.get("blocks/tip/height").await?.parse()?)
}

pub async fn get_tx_status(&self, txid: &Txid) -> Result<TransactionStatus> {
self.get_and_decode(&format!("tx/{txid}/status")).await
self.get_and_decode(&format!("/tx/{txid}/status")).await
}

/// Fetch the transaction at the specified index within the given block.
Expand Down