fix(http): add http:// to a URL whose query holds another URL (#988)

A request URL without a scheme defaults to http://, but the check was `contains("://")`, so `GET localhost:8080/login?next=http://…` was sent as is and failed to parse. Only a `://` after a leading RFC 3986 scheme counts now, both for HTTP Client requests and the proxy connection check.

Co-authored-by: fhrmrk <[email protected]>
Цей коміт міститься в:
Park Jong Hyun authored and GitHub committed 2026-11-03 19:28:21 +08:00
1 джерело ebf2c2f8e3
коміт 839bd0cdb0
2 files changed
+27 -5

No files matched your search

+11 -1
Переглянути файл
@@ -57,6 +57,16 @@ pub fn shared() -> IdeResult<reqwest::Client> {
Ok(client)
}
/// `url`, with `http://` in front unless it starts with a scheme: a `://` later
/// on (`localhost:8080/login?next=http://…`) does not count.
pub(crate) fn with_scheme(url: &str) -> String {
let has_scheme = url.split_once("://").is_some_and(|(scheme, _)| {
scheme.starts_with(|c: char| c.is_ascii_alphabetic())
&& scheme.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
});
if has_scheme { url.to_owned() } else { format!("http://{url}") }
}
/// HTTP Proxy › Check connection: GETs `url` through `proxy` (the settings as
/// edited, not yet applied). Any response but `407` counts as connected.
pub async fn check_connection(proxy: &ProxySettings, url: &str, cancel: &CancellationToken) -> IdeResult<String> {
@@ -64,7 +74,7 @@ pub async fn check_connection(proxy: &ProxySettings, url: &str, cancel: &Cancell
return Err(IdeError::invalid(problem));
}
let url = url.trim();
let url = if url.contains("://") { url.to_owned() } else { format!("http://{url}") };
let url = with_scheme(url);
let client = build(proxy)?;
let request = client.get(&url).timeout(Duration::from_secs(30));
let response = tokio::select! {
+16 -4
Переглянути файл
@@ -1,6 +1,6 @@
//! Preparing (substitution, body includes) and executing requests.
use crate::client::describe;
use crate::client::{describe, with_scheme};
use crate::env::{Environments, Resolver};
use crate::parse::{BodyPart, RawRequest};
use ide_core::{CancellationToken, IdeError, IdeResult};
@@ -84,10 +84,8 @@ pub fn prepare(
Some((_, host)) => format!("http://{host}{url}"),
None => return Err(IdeError::invalid(format!("relative URL {url} needs a Host header"))),
}
} else if !url.contains("://") {
format!("http://{url}")
} else {
url
with_scheme(&url)
};
Ok(PreparedRequest {
name: raw.block.name.clone(),
@@ -214,6 +212,20 @@ mod tests {
assert_eq!(p.url, "http://localhost:9000/status");
}
#[test]
fn urls_without_a_scheme_default_to_http() {
let url = |text: &str| {
let f = parse(text);
prepare(&f.requests[0], &f.variables, &Environments::default(), None, None).unwrap().url
};
assert_eq!(url("GET localhost:8080/health\n"), "http://localhost:8080/health");
assert_eq!(
url("GET localhost:8080/login?next=http://localhost:3000/\n"),
"http://localhost:8080/login?next=http://localhost:3000/"
);
assert_eq!(url("GET https://api.test/cb?u=http://x\n"), "https://api.test/cb?u=http://x");
}
#[tokio::test]
async fn executes_against_a_local_server() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};