Skip to content

Commit

Permalink
support compress request body
Browse files Browse the repository at this point in the history
  • Loading branch information
zuisong committed Jan 22, 2025
1 parent f75d092 commit f85194a
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 11 deletions.
7 changes: 5 additions & 2 deletions completions/_xh
Original file line number Diff line number Diff line change
Expand Up @@ -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]' \
Expand All @@ -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]' \
Expand Down Expand Up @@ -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[]' \
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions completions/_xh.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion completions/xh.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions completions/xh.elv
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions completions/xh.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion completions/xh.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -98,6 +99,7 @@ module completions {
--no-history-print
--no-quiet
--no-stream
--no-compress
--no-output
--no-download
--no-continue
Expand Down
17 changes: 14 additions & 3 deletions doc/xh.1
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

complete\-bash
complete\-elvish
complete\-fish
complete\-nushell
complete\-powershell
complete\-zsh
man

[possible values: 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.
Expand Down
5 changes: 5 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,

Expand Down
2 changes: 1 addition & 1 deletion src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
27 changes: 25 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/to_curl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
142 changes: 141 additions & 1 deletion tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit f85194a

Please sign in to comment.