mirror of
https://github.com/ducaale/xh.git
synced 2025-05-05 15:32:50 +00:00
support compress request body
This commit is contained in:
parent
f75d09216f
commit
f85194ac1e
@ -49,7 +49,7 @@ none\:"Disable both coloring and formatting"))' \
|
||||
'--http-version=[HTTP version to use]:VERSION:(1.0 1.1 2 2-prior-knowledge)' \
|
||||
'*--resolve=[Override DNS resolution for specific domain to a custom IP]:HOST:ADDRESS:_default' \
|
||||
'--interface=[Bind to a network interface or local IP address]:NAME:_default' \
|
||||
'--generate=[Generate shell completions or man pages]:KIND:(complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh man)' \
|
||||
'()--generate=[Generate shell completions or man pages]:KIND:(complete-bash complete-elvish complete-fish complete-nushell complete-powershell complete-zsh man)' \
|
||||
'-j[(default) Serialize data items from the command line as a JSON object]' \
|
||||
'--json[(default) Serialize data items from the command line as a JSON object]' \
|
||||
'-f[Serialize data items from the command line as form fields]' \
|
||||
@ -69,6 +69,8 @@ none\:"Disable both coloring and formatting"))' \
|
||||
'*--quiet[Do not print to stdout or stderr]' \
|
||||
'-S[Always stream the response body]' \
|
||||
'--stream[Always stream the response body]' \
|
||||
'*-x[Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate]' \
|
||||
'*--compress[Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate]' \
|
||||
'-d[Download the body to a file instead of printing it]' \
|
||||
'--download[Download the body to a file instead of printing it]' \
|
||||
'-c[Resume an interrupted download. Requires --download and --output]' \
|
||||
@ -108,6 +110,7 @@ none\:"Disable both coloring and formatting"))' \
|
||||
'--no-history-print[]' \
|
||||
'--no-quiet[]' \
|
||||
'--no-stream[]' \
|
||||
'--no-compress[]' \
|
||||
'--no-output[]' \
|
||||
'--no-download[]' \
|
||||
'--no-continue[]' \
|
||||
@ -142,7 +145,7 @@ none\:"Disable both coloring and formatting"))' \
|
||||
'--no-help[]' \
|
||||
'-V[Print version]' \
|
||||
'--version[Print version]' \
|
||||
'::raw_method_or_url -- The request URL, preceded by an optional HTTP method:_default' \
|
||||
':raw_method_or_url -- The request URL, preceded by an optional HTTP method:_default' \
|
||||
'*::raw_rest_args -- Optional key-value pairs to be included in the request.:_default' \
|
||||
&& ret=0
|
||||
}
|
||||
|
@ -72,6 +72,8 @@ Register-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock {
|
||||
[CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Do not print to stdout or stderr')
|
||||
[CompletionResult]::new('-S', '-S ', [CompletionResultType]::ParameterName, 'Always stream the response body')
|
||||
[CompletionResult]::new('--stream', '--stream', [CompletionResultType]::ParameterName, 'Always stream the response body')
|
||||
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate')
|
||||
[CompletionResult]::new('--compress', '--compress', [CompletionResultType]::ParameterName, 'Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate')
|
||||
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Download the body to a file instead of printing it')
|
||||
[CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'Download the body to a file instead of printing it')
|
||||
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Resume an interrupted download. Requires --download and --output')
|
||||
@ -111,6 +113,7 @@ Register-ArgumentCompleter -Native -CommandName 'xh' -ScriptBlock {
|
||||
[CompletionResult]::new('--no-history-print', '--no-history-print', [CompletionResultType]::ParameterName, 'no-history-print')
|
||||
[CompletionResult]::new('--no-quiet', '--no-quiet', [CompletionResultType]::ParameterName, 'no-quiet')
|
||||
[CompletionResult]::new('--no-stream', '--no-stream', [CompletionResultType]::ParameterName, 'no-stream')
|
||||
[CompletionResult]::new('--no-compress', '--no-compress', [CompletionResultType]::ParameterName, 'no-compress')
|
||||
[CompletionResult]::new('--no-output', '--no-output', [CompletionResultType]::ParameterName, 'no-output')
|
||||
[CompletionResult]::new('--no-download', '--no-download', [CompletionResultType]::ParameterName, 'no-download')
|
||||
[CompletionResult]::new('--no-continue', '--no-continue', [CompletionResultType]::ParameterName, 'no-continue')
|
||||
|
@ -19,7 +19,7 @@ _xh() {
|
||||
|
||||
case "${cmd}" in
|
||||
xh)
|
||||
opts="-j -f -s -p -h -b -m -v -P -q -S -o -d -c -A -a -F -4 -6 -I -V --json --form --multipart --raw --pretty --format-options --style --response-charset --response-mime --print --headers --body --meta --verbose --debug --all --history-print --quiet --stream --output --download --continue --session --session-read-only --auth-type --auth --bearer --ignore-netrc --offline --check-status --follow --max-redirects --timeout --proxy --verify --cert --cert-key --ssl --native-tls --default-scheme --https --http-version --resolve --interface --ipv4 --ipv6 --ignore-stdin --curl --curl-long --generate --help --no-json --no-form --no-multipart --no-raw --no-pretty --no-format-options --no-style --no-response-charset --no-response-mime --no-print --no-headers --no-body --no-meta --no-verbose --no-debug --no-all --no-history-print --no-quiet --no-stream --no-output --no-download --no-continue --no-session --no-session-read-only --no-auth-type --no-auth --no-bearer --no-ignore-netrc --no-offline --no-check-status --no-follow --no-max-redirects --no-timeout --no-proxy --no-verify --no-cert --no-cert-key --no-ssl --no-native-tls --no-default-scheme --no-https --no-http-version --no-resolve --no-interface --no-ipv4 --no-ipv6 --no-ignore-stdin --no-curl --no-curl-long --no-generate --no-help --version [[METHOD] URL] [REQUEST_ITEM]..."
|
||||
opts="-j -f -s -p -h -b -m -v -P -q -S -x -o -d -c -A -a -F -4 -6 -I -V --json --form --multipart --raw --pretty --format-options --style --response-charset --response-mime --print --headers --body --meta --verbose --debug --all --history-print --quiet --stream --compress --output --download --continue --session --session-read-only --auth-type --auth --bearer --ignore-netrc --offline --check-status --follow --max-redirects --timeout --proxy --verify --cert --cert-key --ssl --native-tls --default-scheme --https --http-version --resolve --interface --ipv4 --ipv6 --ignore-stdin --curl --curl-long --generate --help --no-json --no-form --no-multipart --no-raw --no-pretty --no-format-options --no-style --no-response-charset --no-response-mime --no-print --no-headers --no-body --no-meta --no-verbose --no-debug --no-all --no-history-print --no-quiet --no-stream --no-compress --no-output --no-download --no-continue --no-session --no-session-read-only --no-auth-type --no-auth --no-bearer --no-ignore-netrc --no-offline --no-check-status --no-follow --no-max-redirects --no-timeout --no-proxy --no-verify --no-cert --no-cert-key --no-ssl --no-native-tls --no-default-scheme --no-https --no-http-version --no-resolve --no-interface --no-ipv4 --no-ipv6 --no-ignore-stdin --no-curl --no-curl-long --no-generate --no-help --version <[METHOD] URL> [REQUEST_ITEM]..."
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
@ -69,6 +69,8 @@ set edit:completion:arg-completer[xh] = {|@words|
|
||||
cand --quiet 'Do not print to stdout or stderr'
|
||||
cand -S 'Always stream the response body'
|
||||
cand --stream 'Always stream the response body'
|
||||
cand -x 'Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate'
|
||||
cand --compress 'Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate'
|
||||
cand -d 'Download the body to a file instead of printing it'
|
||||
cand --download 'Download the body to a file instead of printing it'
|
||||
cand -c 'Resume an interrupted download. Requires --download and --output'
|
||||
@ -108,6 +110,7 @@ set edit:completion:arg-completer[xh] = {|@words|
|
||||
cand --no-history-print 'no-history-print'
|
||||
cand --no-quiet 'no-quiet'
|
||||
cand --no-stream 'no-stream'
|
||||
cand --no-compress 'no-compress'
|
||||
cand --no-output 'no-output'
|
||||
cand --no-download 'no-download'
|
||||
cand --no-continue 'no-continue'
|
||||
|
@ -35,6 +35,7 @@ complete -c xh -l debug -d 'Print full error stack traces and debug log messages
|
||||
complete -c xh -l all -d 'Show any intermediary requests/responses while following redirects with --follow'
|
||||
complete -c xh -s q -l quiet -d 'Do not print to stdout or stderr'
|
||||
complete -c xh -s S -l stream -d 'Always stream the response body'
|
||||
complete -c xh -s x -l compress -d 'Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate'
|
||||
complete -c xh -s d -l download -d 'Download the body to a file instead of printing it'
|
||||
complete -c xh -s c -l continue -d 'Resume an interrupted download. Requires --download and --output'
|
||||
complete -c xh -l ignore-netrc -d 'Do not use credentials from .netrc'
|
||||
@ -68,6 +69,7 @@ complete -c xh -l no-all
|
||||
complete -c xh -l no-history-print
|
||||
complete -c xh -l no-quiet
|
||||
complete -c xh -l no-stream
|
||||
complete -c xh -l no-compress
|
||||
complete -c xh -l no-output
|
||||
complete -c xh -l no-download
|
||||
complete -c xh -l no-continue
|
||||
|
@ -45,6 +45,7 @@ module completions {
|
||||
--history-print(-P): string # The same as --print but applies only to intermediary requests/responses
|
||||
--quiet(-q) # Do not print to stdout or stderr
|
||||
--stream(-S) # Always stream the response body
|
||||
--compress(-x) # Content compressed (encoded) with Deflate algorithm. The Content-Encoding header is set to deflate
|
||||
--output(-o): string # Save output to FILE instead of stdout
|
||||
--download(-d) # Download the body to a file instead of printing it
|
||||
--continue(-c) # Resume an interrupted download. Requires --download and --output
|
||||
@ -77,7 +78,7 @@ module completions {
|
||||
--curl-long # Use the long versions of curl's flags
|
||||
--generate: string@"nu-complete xh generate" # Generate shell completions or man pages
|
||||
--help # Print help
|
||||
raw_method_or_url?: string # The request URL, preceded by an optional HTTP method
|
||||
raw_method_or_url: string # The request URL, preceded by an optional HTTP method
|
||||
...raw_rest_args: string # Optional key-value pairs to be included in the request.
|
||||
--no-json
|
||||
--no-form
|
||||
@ -98,6 +99,7 @@ module completions {
|
||||
--no-history-print
|
||||
--no-quiet
|
||||
--no-stream
|
||||
--no-compress
|
||||
--no-output
|
||||
--no-download
|
||||
--no-continue
|
||||
|
17
doc/xh.1
17
doc/xh.1
@ -1,4 +1,4 @@
|
||||
.TH XH 1 2025-01-04 0.23.1 "User Commands"
|
||||
.TH XH 1 2025-01-22 0.23.1 "User Commands"
|
||||
|
||||
.SH NAME
|
||||
xh \- Friendly and fast tool for sending HTTP requests
|
||||
@ -188,6 +188,9 @@ Using quiet twice i.e. \-qq will suppress warnings as well.
|
||||
\fB\-S\fR, \fB\-\-stream\fR
|
||||
Always stream the response body.
|
||||
.TP 4
|
||||
\fB\-x\fR, \fB\-\-compress\fR
|
||||
Content compressed (encoded) with Deflate algorithm. The Content\-Encoding header is set to deflate.
|
||||
.TP 4
|
||||
\fB\-o\fR, \fB\-\-output\fR=\fIFILE\fR
|
||||
Save output to FILE instead of stdout.
|
||||
.TP 4
|
||||
@ -321,9 +324,17 @@ For translating the other way, try https://curl2httpie.online/.
|
||||
Use the long versions of curl's flags.
|
||||
.TP 4
|
||||
\fB\-\-generate\fR=\fIKIND\fR
|
||||
Generate shell completions or man pages.
|
||||
Generate shell completions or man pages. Possible values are:
|
||||
|
||||
[possible values: complete\-bash, complete\-elvish, complete\-fish, complete\-nushell, complete\-powershell, complete\-zsh, man]
|
||||
complete\-bash
|
||||
complete\-elvish
|
||||
complete\-fish
|
||||
complete\-nushell
|
||||
complete\-powershell
|
||||
complete\-zsh
|
||||
man
|
||||
|
||||
Example: xh \-\-generate=complete\-bash > xh.bash.
|
||||
.TP 4
|
||||
\fB\-\-help\fR
|
||||
Print help.
|
||||
|
@ -182,6 +182,11 @@ Example: --print=Hb"
|
||||
#[clap(short = 'S', long = "stream", name = "stream")]
|
||||
pub stream_raw: bool,
|
||||
|
||||
/// Content compressed (encoded) with Deflate algorithm.
|
||||
/// The Content-Encoding header is set to deflate.
|
||||
#[clap(short = 'x', long = "compress", name = "compress", action = ArgAction::Count)]
|
||||
pub compress: u8,
|
||||
|
||||
#[clap(skip)]
|
||||
pub stream: Option<bool>,
|
||||
|
||||
|
@ -60,7 +60,7 @@ fn get_file_name(response: &Response, orig_url: &reqwest::Url) -> String {
|
||||
.or_else(|| from_url(orig_url))
|
||||
.unwrap_or_else(|| "index".to_string());
|
||||
|
||||
let filename = filename.split(std::path::is_separator).last().unwrap();
|
||||
let filename = filename.split(std::path::is_separator).next_back().unwrap();
|
||||
|
||||
let mut filename = filename.trim().trim_start_matches('.').to_string();
|
||||
|
||||
|
27
src/main.rs
27
src/main.rs
@ -19,7 +19,7 @@ mod utils;
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, IsTerminal, Read};
|
||||
use std::io::{self, IsTerminal, Read, Write as _};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
@ -28,8 +28,10 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use cookie_store::{CookieStore, RawCookie};
|
||||
use flate2::write::ZlibEncoder;
|
||||
use hyper::header::CONTENT_ENCODING;
|
||||
use redirect::RedirectFollower;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::blocking::{Body as ReqwestBody, Client};
|
||||
use reqwest::header::{
|
||||
HeaderValue, ACCEPT, ACCEPT_ENCODING, CONNECTION, CONTENT_TYPE, COOKIE, RANGE, USER_AGENT,
|
||||
};
|
||||
@ -508,6 +510,27 @@ fn run(args: Cli) -> Result<i32> {
|
||||
|
||||
let mut request = request_builder.headers(headers).build()?;
|
||||
|
||||
if args.compress >= 1 && request.headers().get(CONTENT_ENCODING).is_none() {
|
||||
let mut compressed = false;
|
||||
if let Some(body) = request.body_mut() {
|
||||
if let Some(body_bytes) = body.as_bytes() {
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Default::default());
|
||||
encoder.write_all(body_bytes)?;
|
||||
let output = encoder.finish()?;
|
||||
if output.len() < body_bytes.len() || args.compress >= 2 {
|
||||
let _ = std::mem::replace(body, ReqwestBody::from(output));
|
||||
compressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if compressed {
|
||||
request
|
||||
.headers_mut()
|
||||
.entry(CONTENT_ENCODING)
|
||||
.or_insert(HeaderValue::from_static("deflate"));
|
||||
}
|
||||
}
|
||||
|
||||
for header in &headers_to_unset {
|
||||
request.headers_mut().remove(header);
|
||||
}
|
||||
|
@ -97,6 +97,8 @@ pub fn translate(args: Cli) -> Result<Command> {
|
||||
// No equivalent
|
||||
(args.style.is_some(), "-s/--style"),
|
||||
// No equivalent
|
||||
(args.compress > 0, "-x/--compress"),
|
||||
// No equivalent
|
||||
(args.response_charset.is_some(), "--response-charset"),
|
||||
// No equivalent
|
||||
(args.response_mime.is_some(), "--response-mime"),
|
||||
|
142
tests/cli.rs
142
tests/cli.rs
@ -6,7 +6,7 @@ mod server;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::future::Future;
|
||||
use std::io::Write;
|
||||
use std::io::{Read as _, Write};
|
||||
use std::iter::FromIterator;
|
||||
use std::net::IpAddr;
|
||||
use std::pin::Pin;
|
||||
@ -3483,6 +3483,146 @@ fn zstd() {
|
||||
"#});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compress_request_body() {
|
||||
fn zlib_decode(bytes: Vec<u8>) -> std::io::Result<String> {
|
||||
let mut z = flate2::read::ZlibDecoder::new(&bytes[..]);
|
||||
let mut s = String::new();
|
||||
z.read_to_string(&mut s)?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
let server = server::http(|req| async move {
|
||||
match req.uri().path() {
|
||||
"/deflate" => {
|
||||
assert_eq!(
|
||||
req.headers().get(hyper::header::CONTENT_ENCODING),
|
||||
Some(HeaderValue::from_static("deflate")).as_ref()
|
||||
);
|
||||
|
||||
let compressed_body = req.body().await;
|
||||
let body = zlib_decode(compressed_body).unwrap();
|
||||
hyper::Response::builder()
|
||||
.header("date", "N/A")
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(body.into())
|
||||
.unwrap()
|
||||
}
|
||||
"/normal" => {
|
||||
let body = req.body_as_string().await;
|
||||
hyper::Response::builder()
|
||||
.header("date", "N/A")
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(body.into())
|
||||
.unwrap()
|
||||
}
|
||||
_ => panic!("unknown path"),
|
||||
}
|
||||
});
|
||||
|
||||
get_command()
|
||||
.arg(format!("{}/deflate", server.base_url()))
|
||||
.args([
|
||||
&format!("key={}", "1".repeat(1000)),
|
||||
"-x",
|
||||
"-j",
|
||||
"--pretty=none",
|
||||
])
|
||||
.assert()
|
||||
.stdout(indoc::formatdoc! {r#"
|
||||
HTTP/1.1 200 OK
|
||||
Date: N/A
|
||||
Content-Type: text/plain
|
||||
Content-Length: 1010
|
||||
|
||||
{{"key":"{c}"}}
|
||||
"#, c = "1".repeat(1000),});
|
||||
|
||||
get_command()
|
||||
.arg(format!("{}/deflate", server.base_url()))
|
||||
.args([
|
||||
&format!("key={}", "1".repeat(1000)),
|
||||
"-x",
|
||||
"-x",
|
||||
"-f",
|
||||
"--pretty=none",
|
||||
])
|
||||
.assert()
|
||||
.stdout(indoc::formatdoc! {r#"
|
||||
HTTP/1.1 200 OK
|
||||
Date: N/A
|
||||
Content-Type: text/plain
|
||||
Content-Length: 1004
|
||||
|
||||
key={c}
|
||||
"#, c = "1".repeat(1000),});
|
||||
|
||||
get_command()
|
||||
.arg(format!("{}/deflate", server.base_url()))
|
||||
.args([
|
||||
&format!("key={}", "1".repeat(1000)),
|
||||
"-x",
|
||||
"-x",
|
||||
"-f",
|
||||
"--pretty=none",
|
||||
])
|
||||
.assert()
|
||||
.stdout(indoc::formatdoc! {r#"
|
||||
HTTP/1.1 200 OK
|
||||
Date: N/A
|
||||
Content-Type: text/plain
|
||||
Content-Length: 1004
|
||||
|
||||
key={c}
|
||||
"#, c = "1".repeat(1000),});
|
||||
|
||||
get_command()
|
||||
.arg(format!("{}/normal", server.base_url()))
|
||||
.args([&format!("key={}", "1"), "-x", "-f", "--pretty=none"])
|
||||
.assert()
|
||||
.stdout(indoc::formatdoc! {r#"
|
||||
HTTP/1.1 200 OK
|
||||
Date: N/A
|
||||
Content-Type: text/plain
|
||||
Content-Length: 5
|
||||
|
||||
key={c}
|
||||
"#, c = "1"});
|
||||
|
||||
// force compress
|
||||
get_command()
|
||||
.arg(format!("{}/deflate", server.base_url()))
|
||||
.args([&format!("key={}", "1"), "-xx", "-f", "--pretty=none"])
|
||||
.assert()
|
||||
.stdout(indoc::formatdoc! {r#"
|
||||
HTTP/1.1 200 OK
|
||||
Date: N/A
|
||||
Content-Type: text/plain
|
||||
Content-Length: 5
|
||||
|
||||
key={c}
|
||||
"#, c = "1"});
|
||||
// dont compress_request_body_if_content_encoding_have_value
|
||||
get_command()
|
||||
.arg(format!("{}/normal", server.base_url()))
|
||||
.args([
|
||||
&format!("key={}", "1".repeat(1000)),
|
||||
"content-encoding:gzip",
|
||||
"-x",
|
||||
"-f",
|
||||
"--pretty=none",
|
||||
])
|
||||
.assert()
|
||||
.stdout(indoc::formatdoc! {r#"
|
||||
HTTP/1.1 200 OK
|
||||
Date: N/A
|
||||
Content-Type: text/plain
|
||||
Content-Length: 1004
|
||||
|
||||
key={c}
|
||||
"#, c = "1".repeat(1000),});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_response_with_content_encoding() {
|
||||
let server = server::http(|_req| async move {
|
||||
|
Loading…
x
Reference in New Issue
Block a user