Make http -f display the request headers. Closes #9912 (#10022)

# Description
As described in https://github.com/nushell/nushell/issues/9912, the
`http` command could display the request headers with the `--full` flag,
which could help in debugging the requests. This PR adds such
functionality.

# User-Facing Changes
If `http get` or other `http` command which supports the `--full` flag
is invoked with the flag, it used to display the `headers` key which
contained an table of response headers. Now this key contains two nested
keys: `response` and `request`, each of them being a table of the
response and request headers accordingly.


![image](https://github.com/nushell/nushell/assets/24980/d3cfc4c3-6c27-4634-8552-2cdfbdfc7076)
This commit is contained in:
Eugene Diachkin 2023-08-17 17:19:10 +03:00 committed by GitHub
parent e88a51e930
commit ec5b9b9f37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 48 deletions

View File

@ -464,29 +464,46 @@ fn request_handle_response_content(
requested_url: &str, requested_url: &str,
flags: RequestFlags, flags: RequestFlags,
resp: Response, resp: Response,
request: Request,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let response_headers: Option<PipelineData> = if flags.full { // #response_to_buffer moves "resp" making it impossible to read headers later.
let headers_raw = request_handle_response_headers_raw(span, &resp)?; // Wrapping it into a closure to call when needed
Some(headers_raw) let mut consume_response_body = |response: Response| {
} else { let content_type = response.header("content-type").map(|s| s.to_owned());
None
};
let response_status = resp.status(); match content_type {
let content_type = resp.header("content-type").map(|s| s.to_owned());
let formatted_content = match content_type {
Some(content_type) => transform_response_using_content_type( Some(content_type) => transform_response_using_content_type(
engine_state, engine_state,
stack, stack,
span, span,
requested_url, requested_url,
&flags, &flags,
resp, response,
&content_type, &content_type,
), ),
None => Ok(response_to_buffer(resp, engine_state, span)), None => Ok(response_to_buffer(response, engine_state, span)),
}
}; };
if flags.full { if flags.full {
let response_status = resp.status();
let request_headers_value = match headers_to_nu(&extract_request_headers(&request), span) {
Ok(headers) => headers.into_value(span),
Err(_) => Value::nothing(span),
};
let response_headers_value = match headers_to_nu(&extract_response_headers(&resp), span) {
Ok(headers) => headers.into_value(span),
Err(_) => Value::nothing(span),
};
let headers = Value::Record {
cols: vec!["request".to_string(), "response".to_string()],
vals: vec![request_headers_value, response_headers_value],
span,
};
let full_response = Value::Record { let full_response = Value::Record {
cols: vec![ cols: vec![
"headers".to_string(), "headers".to_string(),
@ -494,19 +511,16 @@ fn request_handle_response_content(
"status".to_string(), "status".to_string(),
], ],
vals: vec![ vals: vec![
match response_headers { headers,
Some(headers) => headers.into_value(span), consume_response_body(resp)?.into_value(span),
None => Value::nothing(span),
},
formatted_content?.into_value(span),
Value::int(response_status as i64, span), Value::int(response_status as i64, span),
], ],
span, span,
} };
.into_pipeline_data();
Ok(full_response) Ok(full_response.into_pipeline_data())
} else { } else {
Ok(formatted_content?) Ok(consume_response_body(resp)?)
} }
} }
@ -517,11 +531,18 @@ pub fn request_handle_response(
requested_url: &str, requested_url: &str,
flags: RequestFlags, flags: RequestFlags,
response: Result<Response, ShellErrorOrRequestError>, response: Result<Response, ShellErrorOrRequestError>,
request: Request,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
match response { match response {
Ok(resp) => { Ok(resp) => request_handle_response_content(
request_handle_response_content(engine_state, stack, span, requested_url, flags, resp) engine_state,
} stack,
span,
requested_url,
flags,
resp,
request,
),
Err(e) => match e { Err(e) => match e {
ShellErrorOrRequestError::ShellError(e) => Err(e), ShellErrorOrRequestError::ShellError(e) => Err(e),
ShellErrorOrRequestError::RequestError(_, e) => { ShellErrorOrRequestError::RequestError(_, e) => {
@ -534,6 +555,7 @@ pub fn request_handle_response(
requested_url, requested_url,
flags, flags,
resp, resp,
request,
)?) )?)
} else { } else {
Err(handle_response_error(span, requested_url, *e)) Err(handle_response_error(span, requested_url, *e))
@ -546,16 +568,39 @@ pub fn request_handle_response(
} }
} }
pub fn request_handle_response_headers_raw( type Headers = HashMap<String, Vec<String>>;
span: Span,
response: &Response,
) -> Result<PipelineData, ShellError> {
let header_names = response.headers_names();
fn extract_request_headers(request: &Request) -> Headers {
request
.header_names()
.iter()
.map(|name| {
(
name.clone(),
request.all(name).iter().map(|e| e.to_string()).collect(),
)
})
.collect()
}
fn extract_response_headers(response: &Response) -> Headers {
response
.headers_names()
.iter()
.map(|name| {
(
name.clone(),
response.all(name).iter().map(|e| e.to_string()).collect(),
)
})
.collect()
}
fn headers_to_nu(headers: &Headers, span: Span) -> Result<PipelineData, ShellError> {
let cols = vec!["name".to_string(), "value".to_string()]; let cols = vec!["name".to_string(), "value".to_string()];
let mut vals = Vec::with_capacity(header_names.len()); let mut vals = Vec::with_capacity(headers.len());
for name in &header_names { for (name, values) in headers {
let is_duplicate = vals.iter().any(|val| { let is_duplicate = vals.iter().any(|val| {
if let Value::Record { vals, .. } = val { if let Value::Record { vals, .. } = val {
if let Some(Value::String { if let Some(Value::String {
@ -568,10 +613,13 @@ pub fn request_handle_response_headers_raw(
false false
}); });
if !is_duplicate { if !is_duplicate {
// Use the ureq `Response.all` api to get all of the header values with a given name. // A single header can hold multiple values
// This interface is why we needed to check if we've already parsed this header name. // This interface is why we needed to check if we've already parsed this header name.
for str_value in response.all(name) { for str_value in values {
let header = vec![Value::string(name, span), Value::string(str_value, span)]; let header = vec![
Value::string(name, span),
Value::string(str_value.to_string(), span),
];
vals.push(Value::record(cols.clone(), header, span)); vals.push(Value::record(cols.clone(), header, span));
} }
} }
@ -585,7 +633,7 @@ pub fn request_handle_response_headers(
response: Result<Response, ShellErrorOrRequestError>, response: Result<Response, ShellErrorOrRequestError>,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
match response { match response {
Ok(resp) => request_handle_response_headers_raw(span, &resp), Ok(resp) => headers_to_nu(&extract_response_headers(&resp), span),
Err(e) => match e { Err(e) => match e {
ShellErrorOrRequestError::ShellError(e) => Err(e), ShellErrorOrRequestError::ShellError(e) => Err(e),
ShellErrorOrRequestError::RequestError(requested_url, e) => { ShellErrorOrRequestError::RequestError(requested_url, e) => {

View File

@ -194,7 +194,7 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, args.data, args.content_type, ctrl_c); let response = send_request(request.clone(), args.data, args.content_type, ctrl_c);
let request_flags = RequestFlags { let request_flags = RequestFlags {
raw: args.raw, raw: args.raw,
@ -209,6 +209,7 @@ fn helper(
&requested_url, &requested_url,
request_flags, request_flags,
response, response,
request,
) )
} }

View File

@ -178,7 +178,7 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, None, None, ctrl_c); let response = send_request(request.clone(), None, None, ctrl_c);
let request_flags = RequestFlags { let request_flags = RequestFlags {
raw: args.raw, raw: args.raw,
@ -193,6 +193,7 @@ fn helper(
&requested_url, &requested_url,
request_flags, request_flags,
response, response,
request,
) )
} }

View File

@ -167,7 +167,7 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, None, None, ctrl_c); let response = send_request(request.clone(), None, None, ctrl_c);
// http options' response always showed in header, so we set full to true. // http options' response always showed in header, so we set full to true.
// And `raw` is useless too because options method doesn't return body, here we set to true // And `raw` is useless too because options method doesn't return body, here we set to true
@ -185,6 +185,7 @@ fn helper(
&requested_url, &requested_url,
request_flags, request_flags,
response, response,
request,
) )
} }

View File

@ -184,7 +184,7 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, Some(args.data), args.content_type, ctrl_c); let response = send_request(request.clone(), Some(args.data), args.content_type, ctrl_c);
let request_flags = RequestFlags { let request_flags = RequestFlags {
raw: args.raw, raw: args.raw,
@ -199,6 +199,7 @@ fn helper(
&requested_url, &requested_url,
request_flags, request_flags,
response, response,
request,
) )
} }

View File

@ -184,7 +184,7 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, Some(args.data), args.content_type, ctrl_c); let response = send_request(request.clone(), Some(args.data), args.content_type, ctrl_c);
let request_flags = RequestFlags { let request_flags = RequestFlags {
raw: args.raw, raw: args.raw,
@ -199,6 +199,7 @@ fn helper(
&requested_url, &requested_url,
request_flags, request_flags,
response, response,
request,
) )
} }

View File

@ -184,7 +184,7 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, Some(args.data), args.content_type, ctrl_c); let response = send_request(request.clone(), Some(args.data), args.content_type, ctrl_c);
let request_flags = RequestFlags { let request_flags = RequestFlags {
raw: args.raw, raw: args.raw,
@ -199,6 +199,7 @@ fn helper(
&requested_url, &requested_url,
request_flags, request_flags,
response, response,
request,
) )
} }

View File

@ -138,9 +138,44 @@ fn http_get_with_custom_headers_as_records() {
"http get -H {{content-type: text/plain}} {url}", "http get -H {{content-type: text/plain}} {url}",
url = server.url() url = server.url()
)); ));
mock1.assert(); mock1.assert();
mock2.assert(); mock2.assert();
} }
#[test]
fn http_get_full_response() {
let mut server = Server::new();
let _mock = server.mock("GET", "/").with_body("foo").create();
let actual = nu!(pipeline(
format!(
"http get --full {url} --headers [foo bar] | to json",
url = server.url()
)
.as_str()
));
let output: serde_json::Value = serde_json::from_str(&actual.out).unwrap();
assert_eq!(output["status"], 200);
assert_eq!(output["body"], "foo");
// There's only one request header, we can get it by index
assert_eq!(output["headers"]["request"][0]["name"], "foo");
assert_eq!(output["headers"]["request"][0]["value"], "bar");
// ... and multiple response headers, so have to search by name
let header = output["headers"]["response"]
.as_array()
.unwrap()
.iter()
.find(|e| e["name"] == "connection")
.unwrap();
assert_eq!(header["value"], "close");
}
// These tests require network access; they use badssl.com which is a Google-affiliated site for testing various SSL errors. // These tests require network access; they use badssl.com which is a Google-affiliated site for testing various SSL errors.
// Revisit this if these tests prove to be flaky or unstable. // Revisit this if these tests prove to be flaky or unstable.