Fix Content-type header override when used in lowercase

This commit is contained in:
jcamiel 2024-02-09 20:49:51 +01:00
parent d2a4bae2f1
commit 9cf13593be
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
10 changed files with 103 additions and 56 deletions

View File

@ -4,6 +4,7 @@ curl --header 'Content-Type: application/json' --data $'{\n "name": "Bob",\n
curl --header 'Content-Type: application/json' --data '{"query":"{\n project(name: \"GraphQL\") {\n tagline\n }\n}"}' 'http://localhost:8000/content-type-json'
curl --header 'Content-Type: application/json' --data $'{\n "name": "Bob",\n "age": 30\n}' 'http://localhost:8000/content-type-json'
curl --header 'Content-Type: application/vnd.api+json' --data $'{\n "name": "Bob",\n "age": 30\n}' 'http://localhost:8000/content-type-vnd-json'
curl --header 'content-type: application/vnd.api+json' --data $'{\n "name": "Bob",\n "age": 30\n}' 'http://localhost:8000/content-type-vnd-json'
curl --data 'field1=foo' --data 'field2=bar' --data 'field2=baz' 'http://localhost:8000/content-type-form'
curl --form 'field1=foo' --form 'field2=bar' --form 'field2=baz' 'http://localhost:8000/content-type-multipart'
curl --header 'Content-Type: application/xml' --data $'<note>\n <to>Tove</to>\n <from>Jani</from>\n <heading>Reminder</heading>\n <body>Don\'t forget me this weekend!</body>\n</note>' 'http://localhost:8000/content-type-xml'

View File

@ -197,7 +197,7 @@
* Implicit content-type=application/json
*
* Request can be run with the following curl command:
* curl --header 'content-type: application/vnd.api+json' --header 'Content-Type: application/json' --data $'{\n "name": "Bob",\n "age": 30\n}' 'http://localhost:8000/content-type-vnd-json'
* curl --header 'content-type: application/vnd.api+json' --data $'{\n "name": "Bob",\n "age": 30\n}' 'http://localhost:8000/content-type-vnd-json'
*
> POST /content-type-vnd-json HTTP/1.1
> Host: localhost:8000

View File

@ -135,10 +135,8 @@ impl Client {
} else {
request_spec
.headers
.iter()
.filter(|header| header.name.to_lowercase() != "authorization")
.cloned()
.collect::<Vec<Header>>()
.retain(|h| h.name.to_lowercase() != AUTHORIZATION.to_lowercase());
request_spec.headers
};
request_spec = RequestSpec {
method: redirect_method,
@ -516,18 +514,19 @@ impl Client {
/// Sets HTTP headers.
fn set_headers(
&mut self,
request: &RequestSpec,
request_spec: &RequestSpec,
options: &ClientOptions,
) -> Result<(), HttpError> {
let mut list = List::new();
for header in &request.headers {
list.append(format!("{}: {}", header.name, header.value).as_str())?;
for header in &request_spec.headers {
list.append(&format!("{}: {}", header.name, header.value))?;
}
// If request has no Content-Type header, we set it if the content type has been set explicitly on this request.
if request.get_header_values(CONTENT_TYPE).is_empty() {
if let Some(s) = &request.content_type {
// If request has no Content-Type header, we set it if the content type has been set
// implicitly on this request.
if !request_spec.headers.contains_key(CONTENT_TYPE) {
if let Some(s) = &request_spec.implicit_content_type {
list.append(&format!("{}: {s}", CONTENT_TYPE))?;
} else {
// We remove default Content-Type headers added by curl because we want
@ -538,17 +537,17 @@ impl Client {
}
}
// Workaround for libcurl issue #11664: When hurl explicitly sets `Expect:` to remove the header,
// Workaround for libcurl issue #11664: When Hurl explicitly sets `Expect:` to remove the header,
// libcurl will generate `SignedHeaders` that include `expect` even though the header is not
// present, causing some APIs to reject the request.
// Therefore we only remove this header when not in aws_sigv4 mode.
if request.get_header_values(EXPECT).is_empty() && options.aws_sigv4.is_none() {
if !request_spec.headers.contains_key(EXPECT) && options.aws_sigv4.is_none() {
// We remove default Expect headers added by curl because we want
// to explicitly manage this header.
list.append(&format!("{}:", EXPECT))?;
}
if request.get_header_values(USER_AGENT).is_empty() {
if !request_spec.headers.contains_key(USER_AGENT) {
let user_agent = match options.user_agent {
Some(ref u) => u.clone(),
None => format!("hurl/{}", clap::crate_version!()),
@ -556,7 +555,7 @@ impl Client {
list.append(&format!("{}: {user_agent}", USER_AGENT))?;
}
if let Some(ref user) = options.user {
if let Some(user) = &options.user {
if options.aws_sigv4.is_some() {
// curl's aws_sigv4 support needs to know the username and password for the
// request, as it uses those values to calculate the Authorization header for the
@ -568,12 +567,12 @@ impl Client {
} else {
let user = user.as_bytes();
let authorization = general_purpose::STANDARD.encode(user);
if request.get_header_values(AUTHORIZATION).is_empty() {
if !request_spec.headers.contains_key(AUTHORIZATION) {
list.append(&format!("{}: Basic {authorization}", AUTHORIZATION))?;
}
}
}
if options.compressed && request.get_header_values(ACCEPT_ENCODING).is_empty() {
if options.compressed && !request_spec.headers.contains_key(ACCEPT_ENCODING) {
list.append(&format!("{}: gzip, deflate, br", ACCEPT_ENCODING))?;
}

View File

@ -92,6 +92,21 @@ impl HeaderVec {
.collect()
}
/// Returns true if there is at least one header with the specified `name`.
pub fn contains_key(&self, name: &str) -> bool {
self.headers
.iter()
.any(|h| h.name.to_lowercase() == name.to_lowercase())
}
/// Retains only the header specified by the predicate.
pub fn retain<F>(&mut self, mut f: F)
where
F: FnMut(&Header) -> bool,
{
self.headers.retain(|h| f(h))
}
/// Returns an iterator over all the headers.
pub fn iter(&self) -> impl Iterator<Item = &Header> {
self.headers.iter()
@ -105,6 +120,11 @@ impl HeaderVec {
self.headers.len()
}
/// Returns true if there is no header.
pub fn is_empty(&self) -> bool {
self.headers.len() == 0
}
/// Push a new `header` into the headers list.
pub fn push(&mut self, header: Header) {
self.headers.push(header)
@ -135,6 +155,7 @@ mod tests {
headers.push(Header::new("baz", "zzz"));
assert_eq!(headers.len(), 5);
assert!(!headers.is_empty());
assert_eq!(headers.get("foo"), Some(&Header::new("foo", "xxx")));
assert_eq!(headers.get("FOO"), Some(&Header::new("foo", "xxx")));
@ -151,6 +172,12 @@ mod tests {
);
assert_eq!(headers.get_all("BAZ"), vec![&Header::new("baz", "zzz")]);
assert_eq!(headers.get_all("qux"), Vec::<&Header>::new());
assert!(headers.contains_key("FOO"));
assert!(!headers.contains_key("fuu"));
headers.retain(|h| h.name.to_lowercase() == "bar");
assert_eq!(headers.len(), 3);
}
#[test]

View File

@ -22,7 +22,7 @@ pub use self::cookie::{CookieAttribute, ResponseCookie};
pub(crate) use self::core::{Cookie, Param, RequestCookie};
pub(crate) use self::error::HttpError;
pub use self::header::{
Header, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, COOKIE, EXPECT, USER_AGENT,
Header, HeaderVec, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, COOKIE, EXPECT, USER_AGENT,
};
pub(crate) use self::options::{ClientOptions, Verbosity};
pub use self::request::{IpResolve, Request, RequestedHttpVersion};

View File

@ -18,7 +18,7 @@
use core::fmt;
use crate::http::core::*;
use crate::http::{header, Header};
use crate::http::header::HeaderVec;
/// Represents the HTTP request asked to be executed by our user (different from the runtime
/// executed HTTP request [`crate::http::Request`].
@ -26,13 +26,17 @@ use crate::http::{header, Header};
pub struct RequestSpec {
pub method: Method,
pub url: String,
pub headers: Vec<Header>,
pub headers: HeaderVec,
pub querystring: Vec<Param>,
pub form: Vec<Param>,
pub multipart: Vec<MultipartParam>,
pub cookies: Vec<RequestCookie>,
pub body: Body,
pub content_type: Option<String>,
/// This is the implicit content type of the request: this content type is implicitly set when
/// the request use a "typed" body: form, JSON, multipart, multiline string with hint. This
/// implicit content type can be different from the user provided one through the `headers`
/// field.
pub implicit_content_type: Option<String>,
}
impl Default for RequestSpec {
@ -40,13 +44,13 @@ impl Default for RequestSpec {
RequestSpec {
method: Method("GET".to_string()),
url: String::new(),
headers: vec![],
headers: HeaderVec::new(),
querystring: vec![],
form: vec![],
multipart: vec![],
cookies: vec![],
body: Body::Binary(vec![]),
content_type: None,
implicit_content_type: None,
}
}
}
@ -85,13 +89,6 @@ impl Body {
}
}
impl RequestSpec {
/// Returns all header values.
pub fn get_header_values(&self, name: &str) -> Vec<String> {
header::get_values(&self.headers, name)
}
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)

View File

@ -36,15 +36,11 @@ impl RequestSpec {
arguments.append(&mut header.curl_args());
}
let has_explicit_content_type = self
.headers
.iter()
.map(|h| &h.name)
.any(|n| n.as_str() == CONTENT_TYPE);
let has_explicit_content_type = self.headers.contains_key(CONTENT_TYPE);
if !has_explicit_content_type {
if let Some(content_type) = &self.content_type {
if content_type.as_str() != "application/x-www-form-urlencoded"
&& content_type.as_str() != "multipart/form-data"
if let Some(content_type) = &self.implicit_content_type {
if content_type != "application/x-www-form-urlencoded"
&& content_type != "multipart/form-data"
{
arguments.push("--header".to_string());
arguments.push(format!("'{}: {content_type}'", CONTENT_TYPE));
@ -158,11 +154,11 @@ impl Method {
impl Header {
pub fn curl_args(&self) -> Vec<String> {
let name = self.name.clone();
let value = self.value.clone();
let name = &self.name;
let value = &self.value;
vec![
"--header".to_string(),
encode_shell_string(format!("{name}: {value}").as_str()),
encode_shell_string(&format!("{name}: {value}")),
]
}
}
@ -261,13 +257,16 @@ pub mod tests {
use super::*;
fn form_http_request() -> RequestSpec {
let mut headers = HeaderVec::new();
headers.push(Header::new(
"Content-Type",
"application/x-www-form-urlencoded",
));
RequestSpec {
method: Method("POST".to_string()),
url: "http://localhost/form-params".to_string(),
headers: vec![Header::new(
"Content-Type",
"application/x-www-form-urlencoded",
)],
headers,
form: vec![
Param {
name: String::from("param1"),
@ -278,7 +277,20 @@ pub mod tests {
value: String::from("a b"),
},
],
content_type: Some("multipart/form-data".to_string()),
implicit_content_type: Some("multipart/form-data".to_string()),
..Default::default()
}
}
fn json_request() -> RequestSpec {
let mut headers = HeaderVec::new();
headers.push(Header::new("content-type", "application/vnd.api+json"));
RequestSpec {
method: Method("POST".to_string()),
url: "http://localhost/json".to_string(),
headers,
body: Body::Text("{\"foo\":\"bar\"}".to_string()),
implicit_content_type: Some("application/json".to_string()),
..Default::default()
}
}
@ -399,6 +411,16 @@ pub mod tests {
"'http://localhost/form-params'".to_string(),
]
);
assert_eq!(
json_request().curl_args(context_dir),
vec![
"--header".to_string(),
"'content-type: application/vnd.api+json'".to_string(),
"--data".to_string(),
"'{\"foo\":\"bar\"}'".to_string(),
"'http://localhost/json'".to_string(),
]
);
}
#[test]

View File

@ -15,7 +15,7 @@
* limitations under the License.
*
*/
use crate::http::{Header, Method, Param, RequestCookie, RequestSpec, Response};
use crate::http::{Header, HeaderVec, Method, Param, RequestCookie, RequestSpec, Response};
/// Some Request Response to be used by tests
@ -138,13 +138,14 @@ pub fn query_http_request() -> RequestSpec {
}
pub fn custom_http_request() -> RequestSpec {
let mut headers = HeaderVec::new();
headers.push(Header::new("User-Agent", "iPhone"));
headers.push(Header::new("Foo", "Bar"));
RequestSpec {
method: Method("GET".to_string()),
url: "http://localhost/custom".to_string(),
headers: vec![
Header::new("User-Agent", "iPhone"),
Header::new("Foo", "Bar"),
],
headers,
cookies: vec![
RequestCookie {
name: String::from("theme"),

View File

@ -280,7 +280,7 @@ fn log_request_spec(request: &http::RequestSpec, logger: &Logger) {
logger.debug(cookie.to_string().as_str());
}
}
if let Some(s) = &request.content_type {
if let Some(s) = &request.implicit_content_type {
logger.debug("");
logger.debug(format!("Implicit content-type={s}").as_str());
}

View File

@ -22,7 +22,7 @@ use base64::Engine;
use hurl_core::ast::*;
use crate::http;
use crate::http::AUTHORIZATION;
use crate::http::{HeaderVec, AUTHORIZATION};
use crate::runner::body::eval_body;
use crate::runner::error::Error;
use crate::runner::multipart::eval_multipart_param;
@ -40,7 +40,7 @@ pub fn eval_request(
let url = eval_template(&request.url, variables)?;
// Headers
let mut headers: Vec<http::Header> = vec![];
let mut headers = HeaderVec::new();
for header in &request.headers {
let name = eval_template(&header.key, variables)?;
let value = eval_template(&header.value, variables)?;
@ -98,7 +98,7 @@ pub fn eval_request(
multipart.push(param);
}
let content_type = if !form.is_empty() {
let implicit_content_type = if !form.is_empty() {
Some("application/x-www-form-urlencoded".to_string())
} else if !multipart.is_empty() {
Some("multipart/form-data".to_string())
@ -130,7 +130,7 @@ pub fn eval_request(
multipart,
cookies,
body,
content_type,
implicit_content_type,
})
}