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
233fn 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}