nu_command/network/http/
post.rs

1use crate::network::http::client::{
2    HttpBody, RequestFlags, RequestMetadata, check_response_redirection, http_client,
3    http_parse_redirect_mode, http_parse_url, request_add_authorization_header,
4    request_add_custom_headers, request_handle_response, request_set_timeout, send_request,
5};
6use nu_engine::command_prelude::*;
7
8#[derive(Clone)]
9pub struct HttpPost;
10
11impl Command for HttpPost {
12    fn name(&self) -> &str {
13        "http post"
14    }
15
16    fn signature(&self) -> Signature {
17        Signature::build("http post")
18            .input_output_types(vec![(Type::Any, Type::Any)])
19            .allow_variants_without_examples(true)
20            .required("URL", SyntaxShape::String, "The URL to post to.")
21            .optional(
22                "data",
23                SyntaxShape::Any,
24                "The contents of the post body. Required unless part of a pipeline.",
25            )
26            .named(
27                "user",
28                SyntaxShape::Any,
29                "the username when authenticating",
30                Some('u'),
31            )
32            .named(
33                "password",
34                SyntaxShape::Any,
35                "the password when authenticating",
36                Some('p'),
37            )
38            .named(
39                "content-type",
40                SyntaxShape::Any,
41                "the MIME type of content to post",
42                Some('t'),
43            )
44            .named(
45                "max-time",
46                SyntaxShape::Duration,
47                "max duration before timeout occurs",
48                Some('m'),
49            )
50            .named(
51                "headers",
52                SyntaxShape::Any,
53                "custom headers you want to add ",
54                Some('H'),
55            )
56            .switch(
57                "raw",
58                "return values as a string instead of a table",
59                Some('r'),
60            )
61            .switch(
62                "insecure",
63                "allow insecure server connections when using SSL",
64                Some('k'),
65            )
66            .switch(
67                "full",
68                "returns the full response instead of only the body",
69                Some('f'),
70            )
71            .switch(
72                "allow-errors",
73                "do not fail if the server returns an error code",
74                Some('e'),
75            )
76            .param(
77                Flag::new("redirect-mode")
78                    .short('R')
79                    .arg(SyntaxShape::String)
80                    .desc(
81                        "What to do when encountering redirects. Default: 'follow'. Valid \
82                         options: 'follow' ('f'), 'manual' ('m'), 'error' ('e').",
83                    )
84                    .completion(nu_protocol::Completion::new_list(
85                        super::client::RedirectMode::MODES,
86                    )),
87            )
88            .filter()
89            .category(Category::Network)
90    }
91
92    fn description(&self) -> &str {
93        "Post a body to a URL."
94    }
95
96    fn extra_description(&self) -> &str {
97        "Performs HTTP POST operation."
98    }
99
100    fn search_terms(&self) -> Vec<&str> {
101        vec!["network", "send", "push"]
102    }
103
104    fn run(
105        &self,
106        engine_state: &EngineState,
107        stack: &mut Stack,
108        call: &Call,
109        input: PipelineData,
110    ) -> Result<PipelineData, ShellError> {
111        run_post(engine_state, stack, call, input)
112    }
113
114    fn examples(&self) -> Vec<Example<'_>> {
115        vec![
116            Example {
117                description: "Post content to example.com",
118                example: "http post https://www.example.com 'body'",
119                result: None,
120            },
121            Example {
122                description: "Post content to example.com, with username and password",
123                example: "http post --user myuser --password mypass https://www.example.com 'body'",
124                result: None,
125            },
126            Example {
127                description: "Post content to example.com, with custom header using a record",
128                example: "http post --headers {my-header-key: my-header-value} https://www.example.com",
129                result: None,
130            },
131            Example {
132                description: "Post content to example.com, with custom header using a list",
133                example: "http post --headers [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com",
134                result: None,
135            },
136            Example {
137                description: "Post content to example.com, with JSON body",
138                example: "http post --content-type application/json https://www.example.com { field: value }",
139                result: None,
140            },
141            Example {
142                description: "Post JSON content from a pipeline to example.com",
143                example: "open --raw foo.json | http post https://www.example.com",
144                result: None,
145            },
146            Example {
147                description: "Upload a binary file to example.com",
148                example: "http post --content-type multipart/form-data https://www.example.com { file: (open -r file.mp3) }",
149                result: None,
150            },
151            Example {
152                description: "Convert a text file into binary and upload it to example.com",
153                example: "http post --content-type multipart/form-data https://www.example.com { file: (open -r file.txt | into binary) }",
154                result: None,
155            },
156            Example {
157                description: "Get the response status code",
158                example: r#"http post https://www.example.com 'body' | metadata | get http_response.status"#,
159                result: None,
160            },
161            Example {
162                description: "Check response status while streaming",
163                example: r#"http post --allow-errors https://example.com/upload 'data' | metadata access {|m| if $m.http_response.status != 200 { error make {msg: "failed"} } else { } } | lines"#,
164                result: None,
165            },
166        ]
167    }
168}
169
170struct Arguments {
171    url: Value,
172    headers: Option<Value>,
173    data: HttpBody,
174    content_type: Option<String>,
175    raw: bool,
176    insecure: bool,
177    user: Option<String>,
178    password: Option<String>,
179    timeout: Option<Value>,
180    full: bool,
181    allow_errors: bool,
182    redirect: Option<Spanned<String>>,
183}
184
185pub fn run_post(
186    engine_state: &EngineState,
187    stack: &mut Stack,
188    call: &Call,
189    input: PipelineData,
190) -> Result<PipelineData, ShellError> {
191    let (data, maybe_metadata) = call
192        .opt::<Value>(engine_state, stack, 1)?
193        .map(|v| (Some(HttpBody::Value(v)), None))
194        .unwrap_or_else(|| match input {
195            PipelineData::Value(v, metadata) => (Some(HttpBody::Value(v)), metadata),
196            PipelineData::ByteStream(byte_stream, metadata) => {
197                (Some(HttpBody::ByteStream(byte_stream)), metadata)
198            }
199            _ => (None, None),
200        });
201    let content_type = call
202        .get_flag(engine_state, stack, "content-type")?
203        .or_else(|| maybe_metadata.and_then(|m| m.content_type));
204
205    let Some(data) = data else {
206        return Err(ShellError::GenericError {
207            error: "Data must be provided either through pipeline or positional argument".into(),
208            msg: "".into(),
209            span: Some(call.head),
210            help: None,
211            inner: vec![],
212        });
213    };
214
215    let args = Arguments {
216        url: call.req(engine_state, stack, 0)?,
217        headers: call.get_flag(engine_state, stack, "headers")?,
218        data,
219        content_type,
220        raw: call.has_flag(engine_state, stack, "raw")?,
221        insecure: call.has_flag(engine_state, stack, "insecure")?,
222        user: call.get_flag(engine_state, stack, "user")?,
223        password: call.get_flag(engine_state, stack, "password")?,
224        timeout: call.get_flag(engine_state, stack, "max-time")?,
225        full: call.has_flag(engine_state, stack, "full")?,
226        allow_errors: call.has_flag(engine_state, stack, "allow-errors")?,
227        redirect: call.get_flag(engine_state, stack, "redirect-mode")?,
228    };
229
230    helper(engine_state, stack, call, args)
231}
232
233// Helper function that actually goes to retrieve the resource from the url given
234// The Option<String> return a possible file extension which can be used in AutoConvert commands
235fn helper(
236    engine_state: &EngineState,
237    stack: &mut Stack,
238    call: &Call,
239    args: Arguments,
240) -> Result<PipelineData, ShellError> {
241    let span = args.url.span();
242    let (requested_url, _) = http_parse_url(call, span, args.url)?;
243    let redirect_mode = http_parse_redirect_mode(args.redirect)?;
244
245    let client = http_client(args.insecure, redirect_mode, engine_state, stack)?;
246    let mut request = client.post(&requested_url);
247
248    request = request_set_timeout(args.timeout, request)?;
249    request = request_add_authorization_header(args.user, args.password, request);
250    request = request_add_custom_headers(args.headers, request)?;
251
252    let (response, request_headers) = send_request(
253        engine_state,
254        request,
255        args.data,
256        args.content_type,
257        call.head,
258        engine_state.signals(),
259    );
260
261    let request_flags = RequestFlags {
262        raw: args.raw,
263        full: args.full,
264        allow_errors: args.allow_errors,
265    };
266
267    let response = response?;
268
269    check_response_redirection(redirect_mode, span, &response)?;
270    request_handle_response(
271        engine_state,
272        stack,
273        RequestMetadata {
274            requested_url: &requested_url,
275            span,
276            headers: request_headers,
277            redirect_mode,
278            flags: request_flags,
279        },
280        response,
281    )
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_examples() {
290        use crate::test_examples;
291
292        test_examples(HttpPost {})
293    }
294}