Allow http commands' automatic redirect-following to be disabled (#11329)

Intends to close #8920 

This PR suggests a new flag for the `http` commands, `--redirect-mode`,
which enables users to choose between different redirect handling modes.
The current behaviour of letting ureq silently follow redirects remains
the default, but two new options are introduced here, following the lead
of [JavaScript's `fetch`
API](https://developer.mozilla.org/en-US/docs/Web/API/fetch#redirect):
"manual", where any 3xx response to a request is simply returned as the
command's result, and "error", where any 3xx response causes a network
error like those caused by 4xx and 5xx responses.

This PR is a draft. Tests have not been added or run, the flag is
currently only implemented for the `http get` command, and design tweaks
are likely to be appropriate.

Most notably, it's not obvious to me whether a single flag which can
take one of three values is the nicest solution here.
We might instead consider two binary flags (like
`--no-following-redirects` and `--disallow-redirects`, although I'm bad
at naming things so I need help with that anyway), or completely drop
the "error" option if it's not deemed useful enough. (I personally think
it has some merit, especially since 4xx and 5xx responses are already
treated as errors by default; So this would allow users to treat only
immediate 2xx responses as success)

# User-facing changes
New options for the `http [method]` commands. Behaviour remains
unchanged when the command line flag introduced here is not used.


![image](https://github.com/nushell/nushell/assets/12228688/1eb89f14-7d48-4f41-8a3e-cc0f1bd0a4f8)
This commit is contained in:
Kira 2023-12-28 08:26:34 +01:00 committed by GitHub
parent 15421dc45e
commit a86a7e6c29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 530 additions and 29 deletions

View File

@ -5,7 +5,8 @@ use base64::{alphabet, Engine};
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
record, BufferedReader, IntoPipelineData, PipelineData, RawStream, ShellError, Span, Value, record, BufferedReader, IntoPipelineData, PipelineData, RawStream, ShellError, Span, Spanned,
Value,
}; };
use ureq::{Error, ErrorKind, Request, Response}; use ureq::{Error, ErrorKind, Request, Response};
@ -26,8 +27,16 @@ pub enum BodyType {
Unknown, Unknown,
} }
#[derive(Clone, Copy, PartialEq)]
pub enum RedirectMode {
Follow,
Error,
Manual,
}
pub fn http_client( pub fn http_client(
allow_insecure: bool, allow_insecure: bool,
redirect_mode: RedirectMode,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<ureq::Agent, ShellError> { ) -> Result<ureq::Agent, ShellError> {
@ -46,6 +55,10 @@ pub fn http_client(
.user_agent("nushell") .user_agent("nushell")
.tls_connector(std::sync::Arc::new(tls)); .tls_connector(std::sync::Arc::new(tls));
if let RedirectMode::Manual | RedirectMode::Error = redirect_mode {
agent_builder = agent_builder.redirects(0);
}
if let Some(http_proxy) = retrieve_http_proxy_from_env(engine_state, stack) { if let Some(http_proxy) = retrieve_http_proxy_from_env(engine_state, stack) {
if let Ok(proxy) = ureq::Proxy::new(http_proxy) { if let Ok(proxy) = ureq::Proxy::new(http_proxy) {
agent_builder = agent_builder.proxy(proxy); agent_builder = agent_builder.proxy(proxy);
@ -72,6 +85,18 @@ pub fn http_parse_url(
Ok((requested_url, url)) Ok((requested_url, url))
} }
pub fn http_parse_redirect_mode(mode: Option<Spanned<String>>) -> Result<RedirectMode, ShellError> {
mode.map_or(Ok(RedirectMode::Follow), |v| match &v.item[..] {
"follow" | "f" => Ok(RedirectMode::Follow),
"error" | "e" => Ok(RedirectMode::Error),
"manual" | "m" => Ok(RedirectMode::Manual),
_ => Err(ShellError::TypeMismatch {
err_message: "Invalid redirect handling mode".to_string(),
span: v.span,
}),
})
}
pub fn response_to_buffer( pub fn response_to_buffer(
response: Response, response: Response,
engine_state: &EngineState, engine_state: &EngineState,
@ -456,6 +481,26 @@ fn transform_response_using_content_type(
}; };
} }
pub fn check_response_redirection(
redirect_mode: RedirectMode,
span: Span,
response: &Result<Response, ShellErrorOrRequestError>,
) -> Result<(), ShellError> {
if let Ok(resp) = response {
if RedirectMode::Error == redirect_mode && (300..400).contains(&resp.status()) {
return Err(ShellError::NetworkFailure {
msg: format!(
"Redirect encountered when redirect handling mode was 'error' ({} {})",
resp.status(),
resp.status_text()
),
span,
});
}
}
Ok(())
}
fn request_handle_response_content( fn request_handle_response_content(
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,

View File

@ -2,12 +2,13 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
use crate::network::http::client::{ use crate::network::http::client::{
http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_handle_response, request_set_timeout, send_request, request_add_authorization_header, request_add_custom_headers, request_handle_response,
request_set_timeout, send_request,
}; };
use super::client::RequestFlags; use super::client::RequestFlags;
@ -79,6 +80,11 @@ impl Command for SubCommand {
"allow-errors", "allow-errors",
"do not fail if the server returns an error code", "do not fail if the server returns an error code",
Some('e'), Some('e'),
).named(
"redirect-mode",
SyntaxShape::String,
"What to do when encountering redirects. Default: 'follow'. Valid options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
Some('R')
) )
.filter() .filter()
.category(Category::Network) .category(Category::Network)
@ -150,6 +156,7 @@ struct Arguments {
timeout: Option<Value>, timeout: Option<Value>,
full: bool, full: bool,
allow_errors: bool, allow_errors: bool,
redirect: Option<Spanned<String>>,
} }
fn run_delete( fn run_delete(
@ -170,6 +177,7 @@ fn run_delete(
timeout: call.get_flag(engine_state, stack, "max-time")?, timeout: call.get_flag(engine_state, stack, "max-time")?,
full: call.has_flag("full"), full: call.has_flag("full"),
allow_errors: call.has_flag("allow-errors"), allow_errors: call.has_flag("allow-errors"),
redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
}; };
helper(engine_state, stack, call, args) helper(engine_state, stack, call, args)
@ -186,8 +194,9 @@ fn helper(
let span = args.url.span(); let span = args.url.span();
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let redirect_mode = http_parse_redirect_mode(args.redirect)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
let mut request = client.delete(&requested_url); let mut request = client.delete(&requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;
@ -202,6 +211,7 @@ fn helper(
allow_errors: args.allow_errors, allow_errors: args.allow_errors,
}; };
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response( request_handle_response(
engine_state, engine_state,
stack, stack,

View File

@ -2,16 +2,15 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
use crate::network::http::client::{ use crate::network::http::client::{
http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_handle_response, request_set_timeout, send_request, request_add_authorization_header, request_add_custom_headers, request_handle_response,
request_set_timeout, send_request, RequestFlags,
}; };
use super::client::RequestFlags;
#[derive(Clone)] #[derive(Clone)]
pub struct SubCommand; pub struct SubCommand;
@ -73,6 +72,12 @@ impl Command for SubCommand {
"do not fail if the server returns an error code", "do not fail if the server returns an error code",
Some('e'), Some('e'),
) )
.named(
"redirect-mode",
SyntaxShape::String,
"What to do when encountering redirects. Default: 'follow'. Valid options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
Some('R')
)
.filter() .filter()
.category(Category::Network) .category(Category::Network)
} }
@ -137,6 +142,7 @@ struct Arguments {
timeout: Option<Value>, timeout: Option<Value>,
full: bool, full: bool,
allow_errors: bool, allow_errors: bool,
redirect: Option<Spanned<String>>,
} }
fn run_get( fn run_get(
@ -155,6 +161,7 @@ fn run_get(
timeout: call.get_flag(engine_state, stack, "max-time")?, timeout: call.get_flag(engine_state, stack, "max-time")?,
full: call.has_flag("full"), full: call.has_flag("full"),
allow_errors: call.has_flag("allow-errors"), allow_errors: call.has_flag("allow-errors"),
redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
}; };
helper(engine_state, stack, call, args) helper(engine_state, stack, call, args)
} }
@ -170,8 +177,9 @@ fn helper(
let span = args.url.span(); let span = args.url.span();
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let redirect_mode = http_parse_redirect_mode(args.redirect)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
let mut request = client.get(&requested_url); let mut request = client.get(&requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;
@ -186,6 +194,7 @@ fn helper(
allow_errors: args.allow_errors, allow_errors: args.allow_errors,
}; };
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response( request_handle_response(
engine_state, engine_state,
stack, stack,

View File

@ -5,12 +5,13 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
use crate::network::http::client::{ use crate::network::http::client::{
http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_handle_response_headers, request_set_timeout, send_request, request_add_authorization_header, request_add_custom_headers, request_handle_response_headers,
request_set_timeout, send_request,
}; };
#[derive(Clone)] #[derive(Clone)]
@ -58,6 +59,11 @@ impl Command for SubCommand {
"insecure", "insecure",
"allow insecure server connections when using SSL", "allow insecure server connections when using SSL",
Some('k'), Some('k'),
).named(
"redirect-mode",
SyntaxShape::String,
"What to do when encountering redirects. Default: 'follow'. Valid options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
Some('R')
) )
.filter() .filter()
.category(Category::Network) .category(Category::Network)
@ -114,6 +120,7 @@ struct Arguments {
user: Option<String>, user: Option<String>,
password: Option<String>, password: Option<String>,
timeout: Option<Value>, timeout: Option<Value>,
redirect: Option<Spanned<String>>,
} }
fn run_head( fn run_head(
@ -129,6 +136,7 @@ fn run_head(
user: call.get_flag(engine_state, stack, "user")?, user: call.get_flag(engine_state, stack, "user")?,
password: call.get_flag(engine_state, stack, "password")?, password: call.get_flag(engine_state, stack, "password")?,
timeout: call.get_flag(engine_state, stack, "max-time")?, timeout: call.get_flag(engine_state, stack, "max-time")?,
redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
}; };
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
@ -146,8 +154,9 @@ fn helper(
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let span = args.url.span(); let span = args.url.span();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let redirect_mode = http_parse_redirect_mode(args.redirect)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
let mut request = client.head(&requested_url); let mut request = client.head(&requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;
@ -155,6 +164,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, None, None, ctrlc); let response = send_request(request, None, None, ctrlc);
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response_headers(span, response) request_handle_response_headers(span, response)
} }

View File

@ -10,7 +10,7 @@ use crate::network::http::client::{
request_handle_response, request_set_timeout, send_request, request_handle_response, request_set_timeout, send_request,
}; };
use super::client::RequestFlags; use super::client::{RedirectMode, RequestFlags};
#[derive(Clone)] #[derive(Clone)]
pub struct SubCommand; pub struct SubCommand;
@ -160,7 +160,7 @@ fn helper(
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, RedirectMode::Follow, engine_state, stack)?;
let mut request = client.request("OPTIONS", &requested_url); let mut request = client.request("OPTIONS", &requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;

View File

@ -2,12 +2,13 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
use crate::network::http::client::{ use crate::network::http::client::{
http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_handle_response, request_set_timeout, send_request, request_add_authorization_header, request_add_custom_headers, request_handle_response,
request_set_timeout, send_request,
}; };
use super::client::RequestFlags; use super::client::RequestFlags;
@ -75,6 +76,11 @@ impl Command for SubCommand {
"allow-errors", "allow-errors",
"do not fail if the server returns an error code", "do not fail if the server returns an error code",
Some('e'), Some('e'),
).named(
"redirect-mode",
SyntaxShape::String,
"What to do when encountering redirects. Default: 'follow'. Valid options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
Some('R')
) )
.filter() .filter()
.category(Category::Network) .category(Category::Network)
@ -142,6 +148,7 @@ struct Arguments {
timeout: Option<Value>, timeout: Option<Value>,
full: bool, full: bool,
allow_errors: bool, allow_errors: bool,
redirect: Option<Spanned<String>>,
} }
fn run_patch( fn run_patch(
@ -162,6 +169,7 @@ fn run_patch(
timeout: call.get_flag(engine_state, stack, "max-time")?, timeout: call.get_flag(engine_state, stack, "max-time")?,
full: call.has_flag("full"), full: call.has_flag("full"),
allow_errors: call.has_flag("allow-errors"), allow_errors: call.has_flag("allow-errors"),
redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
}; };
helper(engine_state, stack, call, args) helper(engine_state, stack, call, args)
@ -178,8 +186,9 @@ fn helper(
let span = args.url.span(); let span = args.url.span();
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let redirect_mode = http_parse_redirect_mode(args.redirect)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
let mut request = client.patch(&requested_url); let mut request = client.patch(&requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;
@ -194,6 +203,7 @@ fn helper(
allow_errors: args.allow_errors, allow_errors: args.allow_errors,
}; };
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response( request_handle_response(
engine_state, engine_state,
stack, stack,

View File

@ -2,12 +2,13 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
use crate::network::http::client::{ use crate::network::http::client::{
http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_handle_response, request_set_timeout, send_request, request_add_authorization_header, request_add_custom_headers, request_handle_response,
request_set_timeout, send_request,
}; };
use super::client::RequestFlags; use super::client::RequestFlags;
@ -75,6 +76,11 @@ impl Command for SubCommand {
"allow-errors", "allow-errors",
"do not fail if the server returns an error code", "do not fail if the server returns an error code",
Some('e'), Some('e'),
).named(
"redirect-mode",
SyntaxShape::String,
"What to do when encountering redirects. Default: 'follow'. Valid options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
Some('R')
) )
.filter() .filter()
.category(Category::Network) .category(Category::Network)
@ -140,6 +146,7 @@ struct Arguments {
timeout: Option<Value>, timeout: Option<Value>,
full: bool, full: bool,
allow_errors: bool, allow_errors: bool,
redirect: Option<Spanned<String>>,
} }
fn run_post( fn run_post(
@ -160,6 +167,7 @@ fn run_post(
timeout: call.get_flag(engine_state, stack, "max-time")?, timeout: call.get_flag(engine_state, stack, "max-time")?,
full: call.has_flag("full"), full: call.has_flag("full"),
allow_errors: call.has_flag("allow-errors"), allow_errors: call.has_flag("allow-errors"),
redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
}; };
helper(engine_state, stack, call, args) helper(engine_state, stack, call, args)
@ -176,8 +184,9 @@ fn helper(
let span = args.url.span(); let span = args.url.span();
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let redirect_mode = http_parse_redirect_mode(args.redirect)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
let mut request = client.post(&requested_url); let mut request = client.post(&requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;
@ -192,6 +201,7 @@ fn helper(
allow_errors: args.allow_errors, allow_errors: args.allow_errors,
}; };
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response( request_handle_response(
engine_state, engine_state,
stack, stack,

View File

@ -2,12 +2,13 @@ use nu_engine::CallExt;
use nu_protocol::ast::Call; use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value,
}; };
use crate::network::http::client::{ use crate::network::http::client::{
http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url,
request_handle_response, request_set_timeout, send_request, request_add_authorization_header, request_add_custom_headers, request_handle_response,
request_set_timeout, send_request,
}; };
use super::client::RequestFlags; use super::client::RequestFlags;
@ -75,6 +76,11 @@ impl Command for SubCommand {
"allow-errors", "allow-errors",
"do not fail if the server returns an error code", "do not fail if the server returns an error code",
Some('e'), Some('e'),
).named(
"redirect-mode",
SyntaxShape::String,
"What to do when encountering redirects. Default: 'follow'. Valid options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
Some('R')
) )
.filter() .filter()
.category(Category::Network) .category(Category::Network)
@ -140,6 +146,7 @@ struct Arguments {
timeout: Option<Value>, timeout: Option<Value>,
full: bool, full: bool,
allow_errors: bool, allow_errors: bool,
redirect: Option<Spanned<String>>,
} }
fn run_put( fn run_put(
@ -160,6 +167,7 @@ fn run_put(
timeout: call.get_flag(engine_state, stack, "max-time")?, timeout: call.get_flag(engine_state, stack, "max-time")?,
full: call.has_flag("full"), full: call.has_flag("full"),
allow_errors: call.has_flag("allow-errors"), allow_errors: call.has_flag("allow-errors"),
redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
}; };
helper(engine_state, stack, call, args) helper(engine_state, stack, call, args)
@ -176,8 +184,9 @@ fn helper(
let span = args.url.span(); let span = args.url.span();
let ctrl_c = engine_state.ctrlc.clone(); let ctrl_c = engine_state.ctrlc.clone();
let (requested_url, _) = http_parse_url(call, span, args.url)?; let (requested_url, _) = http_parse_url(call, span, args.url)?;
let redirect_mode = http_parse_redirect_mode(args.redirect)?;
let client = http_client(args.insecure, engine_state, stack)?; let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
let mut request = client.put(&requested_url); let mut request = client.put(&requested_url);
request = request_set_timeout(args.timeout, request)?; request = request_set_timeout(args.timeout, request)?;
@ -192,6 +201,7 @@ fn helper(
allow_errors: args.allow_errors, allow_errors: args.allow_errors,
}; };
check_response_redirection(redirect_mode, span, &response)?;
request_handle_response( request_handle_response(
engine_state, engine_state,
stack, stack,

View File

@ -38,3 +38,68 @@ fn http_delete_failed_due_to_server_error() {
assert!(actual.err.contains("Bad request (400)")) assert!(actual.err.contains("Bad request (400)"))
} }
#[test]
fn http_delete_follows_redirect() {
let mut server = Server::new();
let _mock = server.mock("GET", "/bar").with_body("bar").create();
let _mock = server
.mock("DELETE", "/foo")
.with_status(301)
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!("http delete {url}/foo", url = server.url()).as_str()
));
assert_eq!(&actual.out, "bar");
}
#[test]
fn http_delete_redirect_mode_manual() {
let mut server = Server::new();
let _mock = server
.mock("DELETE", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http delete --redirect-mode manual {url}/foo",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "foo");
}
#[test]
fn http_delete_redirect_mode_error() {
let mut server = Server::new();
let _mock = server
.mock("DELETE", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http delete --redirect-mode error {url}/foo",
url = server.url()
)
.as_str()
));
assert!(&actual.err.contains("nu::shell::network_failure"));
assert!(&actual.err.contains(
"Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)"
));
}

View File

@ -176,6 +176,71 @@ fn http_get_full_response() {
assert_eq!(header["value"], "close"); assert_eq!(header["value"], "close");
} }
#[test]
fn http_get_follows_redirect() {
let mut server = Server::new();
let _mock = server.mock("GET", "/bar").with_body("bar").create();
let _mock = server
.mock("GET", "/foo")
.with_status(301)
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!("http get {url}/foo", url = server.url()).as_str()
));
assert_eq!(&actual.out, "bar");
}
#[test]
fn http_get_redirect_mode_manual() {
let mut server = Server::new();
let _mock = server
.mock("GET", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http get --redirect-mode manual {url}/foo",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "foo");
}
#[test]
fn http_get_redirect_mode_error() {
let mut server = Server::new();
let _mock = server
.mock("GET", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http get --redirect-mode error {url}/foo",
url = server.url()
)
.as_str()
));
assert!(&actual.err.contains("nu::shell::network_failure"));
assert!(&actual.err.contains(
"Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)"
));
}
// 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.

View File

@ -39,3 +39,75 @@ fn http_head_failed_due_to_server_error() {
assert!(actual.err.contains("Bad request (400)")) assert!(actual.err.contains("Bad request (400)"))
} }
#[test]
fn http_head_follows_redirect() {
let mut server = Server::new();
let _mock = server
.mock("HEAD", "/bar")
.with_header("bar", "bar")
.create();
let _mock = server
.mock("HEAD", "/foo")
.with_status(301)
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http head {url}/foo | (where name == bar).0.value",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "bar");
}
#[test]
fn http_head_redirect_mode_manual() {
let mut server = Server::new();
let _mock = server
.mock("HEAD", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http head --redirect-mode manual {url}/foo | (where name == location).0.value",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "/bar");
}
#[test]
fn http_head_redirect_mode_error() {
let mut server = Server::new();
let _mock = server
.mock("HEAD", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http head --redirect-mode error {url}/foo",
url = server.url()
)
.as_str()
));
assert!(&actual.err.contains("nu::shell::network_failure"));
assert!(&actual.err.contains(
"Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)"
));
}

View File

@ -76,3 +76,68 @@ fn http_patch_failed_due_to_unexpected_body() {
assert!(actual.err.contains("Cannot make request")) assert!(actual.err.contains("Cannot make request"))
} }
#[test]
fn http_patch_follows_redirect() {
let mut server = Server::new();
let _mock = server.mock("GET", "/bar").with_body("bar").create();
let _mock = server
.mock("PATCH", "/foo")
.with_status(301)
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!("http patch {url}/foo patchbody", url = server.url()).as_str()
));
assert_eq!(&actual.out, "bar");
}
#[test]
fn http_patch_redirect_mode_manual() {
let mut server = Server::new();
let _mock = server
.mock("PATCH", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http patch --redirect-mode manual {url}/foo patchbody",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "foo");
}
#[test]
fn http_patch_redirect_mode_error() {
let mut server = Server::new();
let _mock = server
.mock("PATCH", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http patch --redirect-mode error {url}/foo patchbody",
url = server.url()
)
.as_str()
));
assert!(&actual.err.contains("nu::shell::network_failure"));
assert!(&actual.err.contains(
"Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)"
));
}

View File

@ -112,3 +112,68 @@ fn http_post_json_list_is_success() {
mock.assert(); mock.assert();
assert!(actual.out.is_empty()) assert!(actual.out.is_empty())
} }
#[test]
fn http_post_follows_redirect() {
let mut server = Server::new();
let _mock = server.mock("GET", "/bar").with_body("bar").create();
let _mock = server
.mock("POST", "/foo")
.with_status(301)
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!("http post {url}/foo postbody", url = server.url()).as_str()
));
assert_eq!(&actual.out, "bar");
}
#[test]
fn http_post_redirect_mode_manual() {
let mut server = Server::new();
let _mock = server
.mock("POST", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http post --redirect-mode manual {url}/foo postbody",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "foo");
}
#[test]
fn http_post_redirect_mode_error() {
let mut server = Server::new();
let _mock = server
.mock("POST", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http post --redirect-mode error {url}/foo postbody",
url = server.url()
)
.as_str()
));
assert!(&actual.err.contains("nu::shell::network_failure"));
assert!(&actual.err.contains(
"Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)"
));
}

View File

@ -76,3 +76,68 @@ fn http_put_failed_due_to_unexpected_body() {
assert!(actual.err.contains("Cannot make request")) assert!(actual.err.contains("Cannot make request"))
} }
#[test]
fn http_put_follows_redirect() {
let mut server = Server::new();
let _mock = server.mock("GET", "/bar").with_body("bar").create();
let _mock = server
.mock("PUT", "/foo")
.with_status(301)
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!("http put {url}/foo putbody", url = server.url()).as_str()
));
assert_eq!(&actual.out, "bar");
}
#[test]
fn http_put_redirect_mode_manual() {
let mut server = Server::new();
let _mock = server
.mock("PUT", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http put --redirect-mode manual {url}/foo putbody",
url = server.url()
)
.as_str()
));
assert_eq!(&actual.out, "foo");
}
#[test]
fn http_put_redirect_mode_error() {
let mut server = Server::new();
let _mock = server
.mock("PUT", "/foo")
.with_status(301)
.with_body("foo")
.with_header("Location", "/bar")
.create();
let actual = nu!(pipeline(
format!(
"http put --redirect-mode error {url}/foo putbody",
url = server.url()
)
.as_str()
));
assert!(&actual.err.contains("nu::shell::network_failure"));
assert!(&actual.err.contains(
"Redirect encountered when redirect handling mode was 'error' (301 Moved Permanently)"
));
}