mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-23 00:44:55 +03:00
Fix Content-type header override when used in lowercase
This commit is contained in:
parent
d2a4bae2f1
commit
9cf13593be
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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))?;
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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};
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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"),
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user