i suspect some PDSs are behind reverse proxies that normalize the request URL, but it seems tangled's doesn't, so it blows up when creating a session with:
[2025-10-04T20:22:32Z DEBUG reqwest::connect] starting new connection: https://tngl.sh/
Error: Http(HttpError { status: 404, body: Some(b"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot POST //xrpc/com.atproto.server.createSession</pre>\n</body>\n</html>\n") })
note the double-slash at the request url //xrpc/com.atproto.server.createSession
full repro script
use clap::Parser;
use url::Url;
use jacquard::{
api::com_atproto::server::create_session::CreateSession,
client::{AuthenticatedClient, Session, XrpcClient},
};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Args {
/// pds of the bot user
#[arg(short, long, env = "BOT_PDS")]
pds: Url,
/// handle or did of the bot user
#[arg(short, long, env = "BOT_HANDLE")]
identifier: String,
/// app password for the bot user
#[arg(short, long, env = "BOT_APP_PASSWORD")]
app_password: String,
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
let args = Args::parse();
// Create HTTP client
let mut client = AuthenticatedClient::new(reqwest::Client::new(), args.pds.to_string().into());
// Create session
let session = Session::from(
client
.send(
CreateSession::new()
.identifier(args.identifier)
.password(args.app_password)
.build(),
)
.await?
.into_output()?,
);
println!("logged in as {} ({})", session.handle, session.did);
client.set_session(session);
Ok(())
}
oh, or more likely: my
.to_string()from a URL for the PDS must be appending a/