use super::{Ctx, get_hostname};
use bytes::BytesMut;
use http::header;
use http::{HeaderName, HeaderValue};
use pingora::http::RequestHeader;
use pingora::proxy::Session;
use snafu::{ResultExt, Snafu};
use std::borrow::Cow;
use std::fmt::Write;
use std::str::FromStr;
const HTTP_HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
const HTTP_HEADER_X_REAL_IP: &str = "x-real-ip";
pub const HOST_NAME_TAG: &[u8] = b"$hostname";
const HOST_TAG: &[u8] = b"$host";
const SCHEME_TAG: &[u8] = b"$scheme";
const REMOTE_ADDR_TAG: &[u8] = b"$remote_addr";
const REMOTE_PORT_TAG: &[u8] = b"$remote_port";
const SERVER_ADDR_TAG: &[u8] = b"$server_addr";
const SERVER_PORT_TAG: &[u8] = b"$server_port";
const PROXY_ADD_FORWARDED_TAG: &[u8] = b"$proxy_add_x_forwarded_for";
const UPSTREAM_ADDR_TAG: &[u8] = b"$upstream_addr";
static SCHEME_HTTPS: HeaderValue = HeaderValue::from_static("https");
static SCHEME_HTTP: HeaderValue = HeaderValue::from_static("http");
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("invalid header value: {value} - {source}"))]
InvalidHeaderValue {
value: String,
source: header::InvalidHeaderValue,
},
#[snafu(display("invalid header name: {value} - {source}"))]
InvalidHeaderName {
value: String,
source: header::InvalidHeaderName,
},
}
type Result<T, E = Error> = std::result::Result<T, E>;
pub type HttpHeader = (HeaderName, HeaderValue);
pub fn get_host(header: &RequestHeader) -> Option<&str> {
if let Some(host) = header.uri.host() {
return Some(host);
}
header
.headers
.get(http::header::HOST)
.and_then(|value| value.to_str().ok())
.and_then(|host| host.split(':').next())
}
pub fn convert_header(value: &str) -> Result<Option<HttpHeader>> {
value
.split_once(':')
.map(|(k, v)| {
let name = HeaderName::from_str(k.trim())
.context(InvalidHeaderNameSnafu { value: k })?;
let value = HeaderValue::from_str(v.trim())
.context(InvalidHeaderValueSnafu { value: v })?;
Ok(Some((name, value)))
})
.unwrap_or(Ok(None))
}
pub fn convert_headers(header_values: &[String]) -> Result<Vec<HttpHeader>> {
header_values
.iter()
.filter_map(|item| convert_header(item).transpose())
.collect()
}
pub static HTTP_HEADER_NO_STORE: HttpHeader = (
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-store"),
);
pub static HTTP_HEADER_NO_CACHE: HttpHeader = (
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-cache"),
);
pub static HTTP_HEADER_CONTENT_JSON: HttpHeader = (
header::CONTENT_TYPE,
HeaderValue::from_static("application/json; charset=utf-8"),
);
pub static HTTP_HEADER_CONTENT_HTML: HttpHeader = (
header::CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=utf-8"),
);
pub static HTTP_HEADER_CONTENT_TEXT: HttpHeader = (
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
pub static HTTP_HEADER_TRANSFER_CHUNKED: HttpHeader = (
header::TRANSFER_ENCODING,
HeaderValue::from_static("chunked"),
);
pub static HTTP_HEADER_NAME_X_REQUEST_ID: HeaderName =
HeaderName::from_static("x-request-id");
#[inline]
pub fn convert_header_value(
value: &HeaderValue,
session: &Session,
ctx: &Ctx,
) -> Option<HeaderValue> {
let buf = value.as_bytes();
if buf.is_empty() || !(buf[0] == b'$' || buf[0] == b':') {
return None;
}
let to_header_value = |s: &str| HeaderValue::from_str(s).ok();
match buf {
HOST_TAG => get_host(session.req_header()).and_then(to_header_value),
SCHEME_TAG => Some(if ctx.conn.tls_version.is_some() {
SCHEME_HTTPS.clone()
} else {
SCHEME_HTTP.clone()
}),
HOST_NAME_TAG => to_header_value(get_hostname()),
REMOTE_ADDR_TAG => {
ctx.conn.remote_addr.as_deref().and_then(to_header_value)
},
REMOTE_PORT_TAG => ctx.conn.remote_port.and_then(|p| {
HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
}),
SERVER_ADDR_TAG => {
ctx.conn.server_addr.as_deref().and_then(to_header_value)
},
SERVER_PORT_TAG => ctx.conn.server_port.and_then(|p| {
HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
}),
UPSTREAM_ADDR_TAG => {
if !ctx.upstream.address.is_empty() {
to_header_value(&ctx.upstream.address)
} else {
None
}
},
PROXY_ADD_FORWARDED_TAG => {
ctx.conn.remote_addr.as_deref().and_then(|remote_addr| {
let mut value_buf = BytesMut::new();
if let Some(existing) =
session.get_header(HTTP_HEADER_X_FORWARDED_FOR)
{
value_buf.extend_from_slice(existing.as_bytes());
value_buf.extend_from_slice(b", ");
}
value_buf.extend_from_slice(remote_addr.as_bytes());
HeaderValue::from_bytes(&value_buf).ok()
})
},
_ => handle_special_headers(buf, session, ctx),
}
}
#[inline]
fn handle_special_headers(
buf: &[u8],
session: &Session,
ctx: &Ctx,
) -> Option<HeaderValue> {
if buf.starts_with(b"$http_") {
let key = std::str::from_utf8(&buf[6..]).ok()?;
return session.get_header(key).cloned();
}
if buf.starts_with(b"$") {
let var_name = std::str::from_utf8(&buf[1..]).ok()?;
return std::env::var(var_name)
.ok()
.and_then(|v| HeaderValue::from_str(&v).ok());
}
if buf.starts_with(b":") {
let key = std::str::from_utf8(&buf[1..]).ok()?;
let mut value = BytesMut::with_capacity(20);
ctx.append_log_value(&mut value, key);
if !value.is_empty() {
return HeaderValue::from_bytes(&value).ok();
}
}
None
}
pub fn get_remote_addr(session: &Session) -> Option<(String, u16)> {
session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| (addr.ip().to_string(), addr.port()))
}
pub fn get_client_ip(session: &Session) -> String {
if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR) {
if let Ok(s) = value.to_str()
&& let Some(ip) = s.split(',').next()
{
let trimmed_ip = ip.trim();
if !trimmed_ip.is_empty() {
return trimmed_ip.to_string();
}
}
}
if let Some(value) = session.get_header(HTTP_HEADER_X_REAL_IP) {
return value.to_str().unwrap_or_default().to_string();
}
if let Some((addr, _)) = get_remote_addr(session) {
return addr;
}
"".to_string()
}
pub fn get_req_header_value<'a>(
req_header: &'a RequestHeader,
key: &str,
) -> Option<&'a str> {
if let Some(value) = req_header.headers.get(key) {
if let Ok(value) = value.to_str() {
return Some(value);
}
}
None
}
pub fn get_cookie_value<'a>(
req_header: &'a RequestHeader,
cookie_name: &str,
) -> Option<&'a str> {
get_req_header_value(req_header, "cookie")?
.split(';')
.find_map(|item| {
item.trim()
.strip_prefix(cookie_name)?
.strip_prefix('=')
.or_else(|| {
let (k, v) = item.split_once('=')?;
if k.trim() == cookie_name {
Some(v.trim())
} else {
None
}
})
})
}
pub fn get_query_value<'a>(
req_header: &'a RequestHeader,
name: &str,
) -> Option<&'a str> {
req_header
.uri
.query()?
.split('&')
.find_map(|item| {
let (k, v) = item.split_once('=')?;
if k == name { Some(v) } else { None }
})
}
pub fn remove_query_from_header(
req_header: &mut RequestHeader,
name: &str,
) -> Result<(), http::uri::InvalidUri> {
let Some(query_str) = req_header.uri.query() else {
return Ok(());
};
let mut new_query = String::with_capacity(query_str.len());
for item in query_str.split('&') {
let key = item.split('=').next().unwrap_or(item);
if key != name {
if !new_query.is_empty() {
new_query.push('&');
}
new_query.push_str(item);
}
}
let path = req_header.uri.path();
let new_uri_str = if new_query.is_empty() {
Cow::Borrowed(path)
} else {
let mut s = String::with_capacity(path.len() + 1 + new_query.len());
let _ = write!(&mut s, "{}?{}", path, &new_query);
Cow::Owned(s)
};
let new_uri = http::Uri::from_str(&new_uri_str)?;
req_header.set_uri(new_uri);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ConnectionInfo, UpstreamInfo};
use pretty_assertions::assert_eq;
use tokio_test::io::Builder;
#[test]
fn test_convert_headers() {
let headers = convert_headers(&[
"Content-Type: application/octet-stream".to_string(),
"X-Server: $hostname".to_string(),
"X-User: $USER".to_string(),
])
.unwrap();
assert_eq!(3, headers.len());
assert_eq!("content-type", headers[0].0.to_string());
assert_eq!("application/octet-stream", headers[0].1.to_str().unwrap());
assert_eq!("x-server", headers[1].0.to_string());
assert_eq!(false, headers[1].1.to_str().unwrap().is_empty());
assert_eq!("x-user", headers[2].0.to_string());
assert_eq!(false, headers[2].1.to_str().unwrap().is_empty());
}
#[test]
fn test_static_value() {
assert_eq!(
"cache-control: private, no-store",
format!(
"{}: {}",
HTTP_HEADER_NO_STORE.0.to_string(),
HTTP_HEADER_NO_STORE.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"cache-control: private, no-cache",
format!(
"{}: {}",
HTTP_HEADER_NO_CACHE.0.to_string(),
HTTP_HEADER_NO_CACHE.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"content-type: application/json; charset=utf-8",
format!(
"{}: {}",
HTTP_HEADER_CONTENT_JSON.0.to_string(),
HTTP_HEADER_CONTENT_JSON.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"content-type: text/html; charset=utf-8",
format!(
"{}: {}",
HTTP_HEADER_CONTENT_HTML.0.to_string(),
HTTP_HEADER_CONTENT_HTML.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"transfer-encoding: chunked",
format!(
"{}: {}",
HTTP_HEADER_TRANSFER_CHUNKED.0.to_string(),
HTTP_HEADER_TRANSFER_CHUNKED.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"x-request-id",
format!("{}", HTTP_HEADER_NAME_X_REQUEST_ID.to_string(),)
);
assert_eq!(
"content-type: text/plain; charset=utf-8",
format!(
"{}: {}",
HTTP_HEADER_CONTENT_TEXT.0.to_string(),
HTTP_HEADER_CONTENT_TEXT.1.to_str().unwrap_or_default()
)
);
}
#[tokio::test]
async fn test_convert_header_value() {
let headers = ["Host: pingap.io"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let default_state = Ctx {
upstream: UpstreamInfo {
address: "10.1.1.3:4123".to_string(),
..Default::default()
},
conn: ConnectionInfo {
id: 102,
remote_addr: Some("10.1.1.1".to_string()),
remote_port: Some(6000),
server_addr: Some("10.1.1.2".to_string()),
server_port: Some(6001),
tls_version: Some("tls1.3".to_string()),
..Default::default()
},
..Default::default()
};
let value = convert_header_value(
&HeaderValue::from_str("$host").unwrap(),
&session,
&Ctx {
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("pingap.io", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$scheme").unwrap(),
&session,
&Ctx {
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("http", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$scheme").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("https", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$remote_addr").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$remote_port").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("6000", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$server_addr").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.2", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$server_port").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("6001", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$upstream_addr").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.3:4123", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str(":connection_id").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("102", value.unwrap().to_str().unwrap());
let headers = ["X-Forwarded-For: 1.1.1.1, 2.2.2.2"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
&session,
&Ctx {
conn: ConnectionInfo {
remote_addr: Some("10.1.1.1".to_string()),
..Default::default()
},
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!(
"1.1.1.1, 2.2.2.2, 10.1.1.1",
value.unwrap().to_str().unwrap()
);
let headers = [""].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
&session,
&Ctx {
conn: ConnectionInfo {
remote_addr: Some("10.1.1.1".to_string()),
..Default::default()
},
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
let headers = [""].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("$upstream_addr").unwrap(),
&session,
&Ctx {
upstream: UpstreamInfo {
address: "10.1.1.1:8001".to_string(),
..Default::default()
},
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.1:8001", value.unwrap().to_str().unwrap());
let headers = ["Origin: https://github.com"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("$http_origin").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(true, value.is_some());
assert_eq!("https://github.com", value.unwrap().to_str().unwrap());
let headers = ["Origin: https://github.com"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("$hostname").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(true, value.is_some());
let headers = ["Origin: https://github.com"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("$HOME").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(true, value.is_some());
let headers = ["Origin: https://github.com"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
let value = convert_header_value(
&HeaderValue::from_str("UUID").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(false, value.is_some());
}
#[tokio::test]
async fn test_get_host() {
let headers = ["Host: pingap.io"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
assert_eq!(get_host(session.req_header()), Some("pingap.io"));
}
#[test]
fn test_remove_query_from_header() {
let mut req =
RequestHeader::build("GET", b"/?apikey=123", None).unwrap();
remove_query_from_header(&mut req, "apikey").unwrap();
assert_eq!("/", req.uri.to_string());
let mut req =
RequestHeader::build("GET", b"/?apikey=123&name=pingap", None)
.unwrap();
remove_query_from_header(&mut req, "apikey").unwrap();
assert_eq!("/?name=pingap", req.uri.to_string());
}
#[tokio::test]
async fn test_get_client_ip() {
let headers = ["X-Forwarded-For:192.168.1.1"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
assert_eq!(get_client_ip(&session), "192.168.1.1");
let headers = ["X-Real-Ip:192.168.1.2"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
assert_eq!(get_client_ip(&session), "192.168.1.2");
}
#[tokio::test]
async fn test_get_header_value() {
let headers = ["Host: pingap.io"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
assert_eq!(
get_req_header_value(session.req_header(), "Host"),
Some("pingap.io")
);
}
#[tokio::test]
async fn test_get_cookie_value() {
let headers = ["Cookie: name=pingap"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
assert_eq!(
get_cookie_value(session.req_header(), "name"),
Some("pingap")
);
}
#[tokio::test]
async fn test_get_query_value() {
let headers = ["X-Forwarded-For:192.168.1.1"].join("\r\n");
let input_header =
format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
let mock_io = Builder::new().read(input_header.as_bytes()).build();
let mut session = Session::new_h1(Box::new(mock_io));
session.read_request().await.unwrap();
assert_eq!(get_query_value(session.req_header(), "size"), Some("1"));
}
#[test]
fn test_convert_header_edge_cases() {
assert!(convert_header("").unwrap().is_none());
assert!(convert_header("no-colon").unwrap().is_none());
assert!(convert_header("Invalid Name: value").is_err());
assert!(convert_header("Valid-Name: invalid\r\nvalue").is_err());
}
#[test]
fn test_get_host_variants() {
let uri_string = "http://user:pass@authority.com/path";
let uri = http::Uri::from_str(uri_string).unwrap();
let mut req_with_authority =
RequestHeader::build("GET", b"/path", None).unwrap();
req_with_authority.set_uri(uri);
assert_eq!(get_host(&req_with_authority), Some("authority.com"));
let mut req_with_host_header =
RequestHeader::build("GET", b"/path", None).unwrap();
req_with_host_header
.insert_header("Host", "header-host.com:8080")
.unwrap();
assert_eq!(get_host(&req_with_host_header), Some("header-host.com"));
let req_no_host = RequestHeader::build("GET", b"/path", None).unwrap();
assert_eq!(get_host(&req_no_host), None);
}
#[test]
fn test_get_cookie_value_advanced() {
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
req.insert_header("Cookie", "id=123; session=abc; theme=dark")
.unwrap();
assert_eq!(get_cookie_value(&req, "session"), Some("abc"));
assert_eq!(get_cookie_value(&req, "id"), Some("123"));
assert_eq!(get_cookie_value(&req, "theme"), Some("dark"));
assert_eq!(get_cookie_value(&req, "lang"), None);
assert_eq!(get_cookie_value(&req, "the"), None);
}
#[test]
fn test_remove_query_from_header_variants() {
let mut req =
RequestHeader::build("GET", b"/path?key=val", None).unwrap();
remove_query_from_header(&mut req, "key").unwrap();
assert_eq!(req.uri.to_string(), "/path");
let mut req =
RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
.unwrap();
remove_query_from_header(&mut req, "key1").unwrap();
assert_eq!(req.uri.to_string(), "/path?key2=val2");
let mut req =
RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
.unwrap();
remove_query_from_header(&mut req, "key2").unwrap();
assert_eq!(req.uri.to_string(), "/path?key1=val1");
let mut req =
RequestHeader::build("GET", b"/path?key1=v1&key2=v2&key3=v3", None)
.unwrap();
remove_query_from_header(&mut req, "key2").unwrap();
assert_eq!(req.uri.to_string(), "/path?key1=v1&key3=v3");
let mut req =
RequestHeader::build("GET", b"/path?key=val", None).unwrap();
remove_query_from_header(&mut req, "nonexistent").unwrap();
assert_eq!(req.uri.to_string(), "/path?key=val");
let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
remove_query_from_header(&mut req, "key").unwrap();
assert_eq!(req.uri.to_string(), "/path");
}
}