Skip to main content

nexus_web/rest/
request.rs

1//! Sans-IO HTTP request encoder with typestate builder.
2//!
3//! `RequestWriter` is the protocol-level request encoder. It owns a
4//! `WriteBuf` and endpoint configuration (host, default headers, base path).
5//! The typestate builder writes directly into the buffer. `finish()`
6//! returns a `Request<'_>` borrowing the assembled wire bytes.
7//!
8//! No I/O, no sockets, no async — pure state machine.
9//!
10//! ```text
11//! Query   → query()   → Query
12//! Query   → header()  → Headers   (seals request line)
13//! Query   → body()    → Ready     (seals + writes body)
14//! Query   → finish()  → Request<'_>
15//!
16//! Headers → header()  → Headers
17//! Headers → body()    → Ready
18//! Headers → finish()  → Request<'_>
19//!
20//! Ready   → finish()  → Request<'_>
21//! ```
22
23use std::marker::PhantomData;
24
25use super::error::RestError;
26use nexus_net::buf::WriteBuf;
27
28// ---------------------------------------------------------------------------
29// Phase markers
30// ---------------------------------------------------------------------------
31
32/// Request is in the query-parameter phase.
33pub struct Query;
34/// Request is in the headers phase.
35pub struct Headers;
36/// Request is fully assembled, ready to send.
37pub struct Ready;
38
39mod sealed {
40    pub trait Phase {}
41    impl Phase for super::Query {}
42    impl Phase for super::Headers {}
43    impl Phase for super::Ready {}
44}
45
46// ---------------------------------------------------------------------------
47// Method
48// ---------------------------------------------------------------------------
49
50/// HTTP method.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Method {
53    /// `GET` — retrieve a resource.
54    Get,
55    /// `POST` — submit data.
56    Post,
57    /// `PUT` — replace a resource.
58    Put,
59    /// `DELETE` — remove a resource.
60    Delete,
61    /// `PATCH` — partial update.
62    Patch,
63}
64
65impl Method {
66    /// Wire representation.
67    pub fn as_str(self) -> &'static str {
68        match self {
69            Self::Get => "GET",
70            Self::Post => "POST",
71            Self::Put => "PUT",
72            Self::Delete => "DELETE",
73            Self::Patch => "PATCH",
74        }
75    }
76}
77
78impl std::fmt::Display for Method {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.write_str(self.as_str())
81    }
82}
83
84// ---------------------------------------------------------------------------
85// Request (the output — borrows WriteBuf data)
86// ---------------------------------------------------------------------------
87
88/// A built HTTP request. Borrows from the `RequestWriter`'s buffer.
89///
90/// Must be consumed or dropped before the next request on the same writer.
91/// Same lifecycle as [`Message<'_>`](crate::ws::Message) in WebSocket.
92///
93/// `Clone` is cheap (copies a pointer + length, no allocation). Use it
94/// to archive request bytes before sending:
95///
96/// ```ignore
97/// let req = writer.post("/order").body(json).finish()?;
98/// let archived = req.clone();
99/// conn.send(req, &mut reader)?;
100/// archive_log.write(archived.as_bytes());
101/// ```
102#[derive(Clone)]
103pub struct Request<'a> {
104    data: &'a [u8],
105}
106
107impl<'a> Request<'a> {
108    /// The complete HTTP request as wire bytes.
109    pub fn as_bytes(&self) -> &[u8] {
110        self.data
111    }
112
113    /// Consume the request, returning the raw wire bytes.
114    ///
115    /// Releases the borrow on the `RequestWriter` while keeping
116    /// access to the bytes (they remain valid until the writer
117    /// is used again).
118    ///
119    /// ```ignore
120    /// let payload = writer.post("/order").body(json).finish()?.into_bytes();
121    /// archive_log.write(payload);
122    /// ```
123    pub fn into_bytes(self) -> &'a [u8] {
124        self.data
125    }
126
127    /// Request size in bytes.
128    pub fn len(&self) -> usize {
129        self.data.len()
130    }
131
132    /// Whether the request is empty (should never be true after `finish()`).
133    pub fn is_empty(&self) -> bool {
134        self.data.is_empty()
135    }
136}
137
138impl std::fmt::Debug for Request<'_> {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("Request")
141            .field("len", &self.data.len())
142            .finish()
143    }
144}
145
146// ---------------------------------------------------------------------------
147// Percent-encoding (RFC 3986)
148// ---------------------------------------------------------------------------
149
150/// Unreserved characters: A-Z a-z 0-9 - . _ ~
151const UNRESERVED: [bool; 256] = {
152    let mut table = [false; 256];
153    let mut i = 0;
154    while i < 256 {
155        table[i] = matches!(
156            i as u8,
157            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~'
158        );
159        i += 1;
160    }
161    table
162};
163
164const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF";
165
166/// Percent-encode `input` per RFC 3986 directly into the WriteBuf.
167/// Percent-encode `input` per RFC 3986 directly into the WriteBuf.
168/// Batch-scans for runs of unreserved bytes to minimize checked_append calls.
169fn append_percent_encoded(buf: &mut WriteBuf, input: &[u8], error: &mut Option<RestError>) {
170    if error.is_some() {
171        return;
172    }
173    let mut i = 0;
174    while i < input.len() {
175        // Scan for a run of unreserved bytes.
176        let run_start = i;
177        while i < input.len() && UNRESERVED[input[i] as usize] {
178            i += 1;
179        }
180        // Append the unreserved run in one bulk copy.
181        if i > run_start {
182            checked_append(buf, &input[run_start..i], error);
183            if error.is_some() {
184                return;
185            }
186        }
187        // Encode the next reserved byte (if any).
188        if i < input.len() {
189            let b = input[i];
190            checked_append(
191                buf,
192                &[
193                    b'%',
194                    HEX_UPPER[(b >> 4) as usize],
195                    HEX_UPPER[(b & 0xf) as usize],
196                ],
197                error,
198            );
199            if error.is_some() {
200                return;
201            }
202            i += 1;
203        }
204    }
205}
206
207// ---------------------------------------------------------------------------
208// Helpers
209// ---------------------------------------------------------------------------
210
211/// Append to WriteBuf with deferred overflow error.
212fn checked_append(buf: &mut WriteBuf, src: &[u8], error: &mut Option<RestError>) {
213    if error.is_some() {
214        return;
215    }
216    if src.len() > buf.tailroom() {
217        *error = Some(RestError::RequestTooLarge {
218            capacity: buf.len() + buf.tailroom(),
219        });
220        return;
221    }
222    buf.append(src);
223}
224
225/// Check for CR/LF bytes.
226fn has_crlf(s: &str) -> bool {
227    s.bytes().any(|b| b == b'\r' || b == b'\n')
228}
229
230/// Write a usize as ASCII digits without allocation.
231fn write_usize_ascii(buf: &mut WriteBuf, n: usize, error: &mut Option<RestError>) {
232    if n == 0 {
233        checked_append(buf, b"0", error);
234        return;
235    }
236    let mut digits = [0u8; 20]; // max digits for usize
237    let mut i = 20;
238    let mut val = n;
239    while val > 0 {
240        i -= 1;
241        digits[i] = (val % 10) as u8 + b'0';
242        val /= 10;
243    }
244    checked_append(buf, &digits[i..], error);
245}
246
247/// Write " HTTP/1.1\r\n" + host_wire + default_headers_wire.
248fn seal_request_line(writer: &mut RequestWriter, error: &mut Option<RestError>) {
249    checked_append(&mut writer.write_buf, b" HTTP/1.1\r\n", error);
250    // Split borrow: write_buf (mut) and host_wire/default_headers_wire (shared).
251    checked_append(&mut writer.write_buf, &writer.host_wire, error);
252    if !writer.default_headers_wire.is_empty() {
253        checked_append(&mut writer.write_buf, &writer.default_headers_wire, error);
254    }
255}
256
257/// Write Content-Length + \r\n separator + body bytes.
258fn write_body(buf: &mut WriteBuf, body: &[u8], error: &mut Option<RestError>) {
259    checked_append(buf, b"Content-Length: ", error);
260    write_usize_ascii(buf, body.len(), error);
261    checked_append(buf, b"\r\n\r\n", error);
262    checked_append(buf, body, error);
263}
264
265/// Write Content-Length header + \r\n separator (no body).
266fn write_body_header(buf: &mut WriteBuf, body_len: usize, error: &mut Option<RestError>) {
267    checked_append(buf, b"Content-Length: ", error);
268    write_usize_ascii(buf, body_len, error);
269    checked_append(buf, b"\r\n\r\n", error);
270}
271
272/// Content-Length placeholder: "Content-Length: XXXXXXXXXX\r\n\r\n"
273/// 20 digits supports all possible `usize` values on 64-bit.
274const CL_PREFIX: &[u8] = b"Content-Length: ";
275const CL_PAD_LEN: usize = 20;
276const CL_SUFFIX: &[u8] = b"\r\n\r\n";
277/// Write a padded Content-Length placeholder. Returns the offset
278/// within the WriteBuf where the 10-digit number starts.
279fn write_content_length_placeholder(buf: &mut WriteBuf, error: &mut Option<RestError>) -> usize {
280    checked_append(buf, CL_PREFIX, error);
281    let num_offset = buf.len();
282    checked_append(buf, b"00000000000000000000", error); // 20 zeros
283    checked_append(buf, CL_SUFFIX, error);
284    num_offset
285}
286
287/// Write adapter for direct serialization into the request buffer.
288///
289/// Type alias for [`WriteBufWriter`](nexus_net::buf::WriteBufWriter).
290/// Implements `std::io::Write`.
291pub type BodyWriter<'a> = nexus_net::buf::WriteBufWriter<'a>;
292
293/// Backfill the Content-Length placeholder with the actual body length.
294/// Writes exact digits, shifts body left to close the gap.
295///
296/// Produces clean wire format: `Content-Length: 42\r\n\r\n` (no extra spaces).
297fn backfill_content_length(buf: &mut WriteBuf, num_offset: usize, body_len: usize) {
298    // Format the actual digits on the stack.
299    let mut digits = [0u8; 20];
300    let digit_len = if body_len == 0 {
301        digits[0] = b'0';
302        1
303    } else {
304        let mut val = body_len;
305        let mut i = 20;
306        while val > 0 {
307            i -= 1;
308            digits[i] = (val % 10) as u8 + b'0';
309            val /= 10;
310        }
311        let len = 20 - i;
312        digits.copy_within(i..20, 0);
313        len
314    };
315
316    let gap = CL_PAD_LEN - digit_len;
317
318    {
319        let data = buf.data_mut();
320
321        // Overwrite placeholder with actual digits.
322        data[num_offset..num_offset + digit_len].copy_from_slice(&digits[..digit_len]);
323
324        // Write \r\n\r\n right after the digits.
325        let suffix_dst = num_offset + digit_len;
326        data[suffix_dst..suffix_dst + CL_SUFFIX.len()].copy_from_slice(CL_SUFFIX);
327
328        // Shift body bytes left to close the gap.
329        if gap > 0 {
330            let body_start = num_offset + CL_PAD_LEN + CL_SUFFIX.len();
331            let body_end = data.len();
332            if body_start < body_end {
333                data.copy_within(body_start..body_end, body_start - gap);
334            }
335        }
336    }
337
338    // Shrink the buffer to remove the gap bytes.
339    if gap > 0 {
340        buf.shrink_tail(gap);
341    }
342}
343
344// ---------------------------------------------------------------------------
345// RequestWriter
346// ---------------------------------------------------------------------------
347
348/// Sans-IO HTTP request encoder.
349///
350/// Owns a `WriteBuf` and endpoint configuration. The typestate builder
351/// methods serialize HTTP requests directly into the buffer. `finish()`
352/// returns `Request<'_>` borrowing the assembled bytes.
353///
354/// # Usage
355///
356/// ```ignore
357/// use nexus_web::rest::RequestWriter;
358///
359/// let mut writer = RequestWriter::new("api.binance.com")?;
360/// writer.set_base_path("/api/v3")?;
361/// writer.default_header("X-API-KEY", &key)?;
362///
363/// let req = writer.get("/orders")
364///     .query("symbol", "BTC-USD")
365///     .finish()?;
366///
367/// // req.as_bytes() contains the complete HTTP request wire bytes
368/// ```
369pub struct RequestWriter {
370    write_buf: WriteBuf,
371    /// Pre-serialized: "Host: hostname\r\nConnection: keep-alive\r\n"
372    host_wire: Vec<u8>,
373    /// Pre-serialized default headers: "Name: Value\r\n..."
374    default_headers_wire: Vec<u8>,
375    /// Base path prefix prepended to all request paths.
376    base_path: Vec<u8>,
377}
378
379impl RequestWriter {
380    /// Create a new writer for the given host.
381    ///
382    /// Pre-serializes the Host and Connection: keep-alive headers.
383    /// Default write buffer: 32KB.
384    ///
385    /// # Errors
386    ///
387    /// Returns [`RestError::CrlfInjection`] if `host` contains CR/LF.
388    pub fn new(host: &str) -> Result<Self, RestError> {
389        if host.bytes().any(|b| b == b'\r' || b == b'\n') {
390            return Err(RestError::CrlfInjection);
391        }
392
393        let mut host_wire = Vec::with_capacity(host.len() + 32);
394        host_wire.extend_from_slice(b"Host: ");
395        host_wire.extend_from_slice(host.as_bytes());
396        host_wire.extend_from_slice(b"\r\nConnection: keep-alive\r\n");
397
398        Ok(Self {
399            write_buf: WriteBuf::new(32 * 1024, 0),
400            host_wire,
401            default_headers_wire: Vec::new(),
402            base_path: Vec::new(),
403        })
404    }
405
406    /// Set the write buffer capacity. Default: 32KB.
407    ///
408    /// Must be called before any requests are built.
409    ///
410    /// # Panics
411    ///
412    /// Panics if `capacity` is 0.
413    pub fn set_write_buffer_capacity(&mut self, capacity: usize) {
414        self.write_buf = WriteBuf::new(capacity, 0);
415    }
416
417    /// Add a default header sent with every request.
418    ///
419    /// Pre-serializes into wire format. Append-only.
420    ///
421    /// # Errors
422    ///
423    /// Returns [`RestError::CrlfInjection`] if name or value contains CR/LF.
424    pub fn default_header(&mut self, name: &str, value: &str) -> Result<(), RestError> {
425        if has_crlf(name) || has_crlf(value) {
426            return Err(RestError::CrlfInjection);
427        }
428        self.default_headers_wire.extend_from_slice(name.as_bytes());
429        self.default_headers_wire.extend_from_slice(b": ");
430        self.default_headers_wire
431            .extend_from_slice(value.as_bytes());
432        self.default_headers_wire.extend_from_slice(b"\r\n");
433        Ok(())
434    }
435
436    /// Set a base path prefix prepended to all request paths.
437    ///
438    /// Trailing slashes are stripped. Request paths should start with `/`.
439    ///
440    /// # Errors
441    ///
442    /// Returns [`RestError::CrlfInjection`] if the path contains CR/LF.
443    pub fn set_base_path(&mut self, path: &str) -> Result<(), RestError> {
444        if has_crlf(path) {
445            return Err(RestError::CrlfInjection);
446        }
447        self.base_path = path.trim_end_matches('/').as_bytes().to_vec();
448        Ok(())
449    }
450
451    // =========================================================================
452    // Request builders — Query phase
453    // =========================================================================
454
455    /// Build a GET request.
456    pub fn get(&mut self, path: &str) -> RequestBuilder<'_> {
457        self.request(Method::Get, path)
458    }
459
460    /// Build a POST request.
461    pub fn post(&mut self, path: &str) -> RequestBuilder<'_> {
462        self.request(Method::Post, path)
463    }
464
465    /// Build a PUT request.
466    pub fn put(&mut self, path: &str) -> RequestBuilder<'_> {
467        self.request(Method::Put, path)
468    }
469
470    /// Build a DELETE request.
471    pub fn delete(&mut self, path: &str) -> RequestBuilder<'_> {
472        self.request(Method::Delete, path)
473    }
474
475    /// Build a request with the given method.
476    pub fn request(&mut self, method: Method, path: &str) -> RequestBuilder<'_> {
477        RequestBuilder::new(self, method, path)
478    }
479
480    // =========================================================================
481    // Request builders — Headers phase (pre-formed URL)
482    // =========================================================================
483
484    /// Build a GET with a pre-formed URL path (including any query string).
485    ///
486    /// Skips the [`Query`] phase — returns [`Headers`] directly.
487    pub fn get_raw(&mut self, path: &str) -> RequestBuilder<'_, Headers> {
488        self.request_raw(Method::Get, path)
489    }
490
491    /// Build a POST with a pre-formed URL path.
492    pub fn post_raw(&mut self, path: &str) -> RequestBuilder<'_, Headers> {
493        self.request_raw(Method::Post, path)
494    }
495
496    /// Build a PUT with a pre-formed URL path.
497    pub fn put_raw(&mut self, path: &str) -> RequestBuilder<'_, Headers> {
498        self.request_raw(Method::Put, path)
499    }
500
501    /// Build a DELETE with a pre-formed URL path.
502    pub fn delete_raw(&mut self, path: &str) -> RequestBuilder<'_, Headers> {
503        self.request_raw(Method::Delete, path)
504    }
505
506    /// Build a request with a pre-formed URL path.
507    pub fn request_raw(&mut self, method: Method, path: &str) -> RequestBuilder<'_, Headers> {
508        RequestBuilder::new_sealed(self, method, path)
509    }
510}
511
512// ---------------------------------------------------------------------------
513// RequestBuilder
514// ---------------------------------------------------------------------------
515
516/// Typestate request builder. Writes directly into a `RequestWriter`'s
517/// buffer — no intermediate storage, no stream type parameter.
518///
519/// Phase type parameter enforces correct wire ordering at compile time:
520/// query parameters before headers, headers before body.
521#[must_use = "request must be finished with .finish()"]
522pub struct RequestBuilder<'a, P: sealed::Phase = Query> {
523    writer: &'a mut RequestWriter,
524    has_query: bool,
525    error: Option<RestError>,
526    _phase: PhantomData<P>,
527}
528
529// =========================================================================
530// Query phase
531// =========================================================================
532
533impl<'a> RequestBuilder<'a, Query> {
534    pub(crate) fn new(writer: &'a mut RequestWriter, method: Method, path: &str) -> Self {
535        writer.write_buf.clear();
536        let mut error = if has_crlf(path) {
537            Some(RestError::CrlfInjection)
538        } else {
539            None
540        };
541        checked_append(
542            &mut writer.write_buf,
543            method.as_str().as_bytes(),
544            &mut error,
545        );
546        checked_append(&mut writer.write_buf, b" ", &mut error);
547        if !writer.base_path.is_empty() {
548            checked_append(&mut writer.write_buf, &writer.base_path, &mut error);
549        }
550        checked_append(&mut writer.write_buf, path.as_bytes(), &mut error);
551        Self {
552            writer,
553            has_query: path.contains('?'),
554            error,
555            _phase: PhantomData,
556        }
557    }
558
559    pub(crate) fn new_sealed(
560        writer: &'a mut RequestWriter,
561        method: Method,
562        path: &str,
563    ) -> RequestBuilder<'a, Headers> {
564        writer.write_buf.clear();
565        let mut error = if has_crlf(path) {
566            Some(RestError::CrlfInjection)
567        } else {
568            None
569        };
570        checked_append(
571            &mut writer.write_buf,
572            method.as_str().as_bytes(),
573            &mut error,
574        );
575        checked_append(&mut writer.write_buf, b" ", &mut error);
576        if !writer.base_path.is_empty() {
577            checked_append(&mut writer.write_buf, &writer.base_path, &mut error);
578        }
579        checked_append(&mut writer.write_buf, path.as_bytes(), &mut error);
580        seal_request_line(writer, &mut error);
581        RequestBuilder {
582            writer,
583            has_query: false,
584            error,
585            _phase: PhantomData,
586        }
587    }
588
589    /// Add a query parameter. Percent-encodes key and value per RFC 3986.
590    pub fn query(mut self, key: &str, value: &str) -> Self {
591        let sep = if self.has_query { b"&" as &[u8] } else { b"?" };
592        checked_append(&mut self.writer.write_buf, sep, &mut self.error);
593        append_percent_encoded(&mut self.writer.write_buf, key.as_bytes(), &mut self.error);
594        checked_append(&mut self.writer.write_buf, b"=", &mut self.error);
595        append_percent_encoded(
596            &mut self.writer.write_buf,
597            value.as_bytes(),
598            &mut self.error,
599        );
600        self.has_query = true;
601        self
602    }
603
604    /// Add a pre-encoded query parameter. No percent-encoding applied.
605    ///
606    /// Caller is responsible for correct encoding. Validates no CR/LF.
607    pub fn query_raw(mut self, key: &str, value: &str) -> Self {
608        if has_crlf(key) || has_crlf(value) {
609            self.error = Some(RestError::CrlfInjection);
610            return self;
611        }
612        let sep = if self.has_query { b"&" as &[u8] } else { b"?" };
613        checked_append(&mut self.writer.write_buf, sep, &mut self.error);
614        checked_append(&mut self.writer.write_buf, key.as_bytes(), &mut self.error);
615        checked_append(&mut self.writer.write_buf, b"=", &mut self.error);
616        checked_append(
617            &mut self.writer.write_buf,
618            value.as_bytes(),
619            &mut self.error,
620        );
621        self.has_query = true;
622        self
623    }
624
625    /// Add a request header. Transitions to the headers phase.
626    pub fn header(mut self, name: &str, value: &str) -> RequestBuilder<'a, Headers> {
627        seal_request_line(self.writer, &mut self.error);
628        let mut next = RequestBuilder {
629            writer: self.writer,
630            has_query: self.has_query,
631            error: self.error,
632            _phase: PhantomData,
633        };
634        next.append_header(name, value);
635        next
636    }
637
638    /// Set the request body. Transitions to the ready phase.
639    pub fn body(mut self, body: &[u8]) -> RequestBuilder<'a, Ready> {
640        seal_request_line(self.writer, &mut self.error);
641        write_body(&mut self.writer.write_buf, body, &mut self.error);
642        RequestBuilder {
643            writer: self.writer,
644            has_query: self.has_query,
645            error: self.error,
646            _phase: PhantomData,
647        }
648    }
649
650    /// Write the body directly into the buffer via a closure.
651    ///
652    /// Zero-alloc: the closure writes into the WriteBuf's spare region
653    /// via [`BodyWriter`] (implements `std::io::Write`). Content-Length
654    /// is computed and backfilled automatically.
655    ///
656    /// ```ignore
657    /// let req = writer.post("/order")
658    ///     .body_writer(|w| serde_json::to_writer(w, &order))
659    ///     .finish()?;
660    /// ```
661    pub fn body_writer<F, E>(mut self, f: F) -> RequestBuilder<'a, Ready>
662    where
663        F: FnOnce(&mut BodyWriter<'_>) -> Result<(), E>,
664        E: Into<Box<dyn std::error::Error + Send + Sync>>,
665    {
666        seal_request_line(self.writer, &mut self.error);
667        if self.error.is_some() {
668            return RequestBuilder {
669                writer: self.writer,
670                has_query: self.has_query,
671                error: self.error,
672                _phase: PhantomData,
673            };
674        }
675        let num_offset =
676            write_content_length_placeholder(&mut self.writer.write_buf, &mut self.error);
677        if self.error.is_some() {
678            return RequestBuilder {
679                writer: self.writer,
680                has_query: self.has_query,
681                error: self.error,
682                _phase: PhantomData,
683            };
684        }
685        let body_len = {
686            let mut bw = BodyWriter::new(&mut self.writer.write_buf);
687            if let Err(e) = f(&mut bw) {
688                // If the buffer is full, this is a capacity issue.
689                self.error = Some(if self.writer.write_buf.tailroom() == 0 {
690                    RestError::RequestTooLarge {
691                        capacity: self.writer.write_buf.len() + self.writer.write_buf.tailroom(),
692                    }
693                } else {
694                    RestError::Io(std::io::Error::other(e))
695                });
696                0
697            } else {
698                bw.written()
699            }
700        }; // bw dropped here, releasing write_buf borrow
701        if self.error.is_none() {
702            backfill_content_length(&mut self.writer.write_buf, num_offset, body_len);
703        }
704        RequestBuilder {
705            writer: self.writer,
706            has_query: self.has_query,
707            error: self.error,
708            _phase: PhantomData,
709        }
710    }
711
712    /// Write a fixed-size body via closure with direct `&mut [u8]` access.
713    ///
714    /// The caller specifies the exact body size upfront. Content-Length
715    /// is written with exact digits (no backfill). The closure receives
716    /// a zeroed `&mut [u8]` slice of exactly `len` bytes to write into.
717    ///
718    /// For binary wire formats (SBE, FIX, protobuf) where the message
719    /// size is known at construction time.
720    ///
721    /// ```ignore
722    /// let req = writer.post("/order")
723    ///     .body_fixed(128, |buf| {
724    ///         order.encode_sbe(buf);
725    ///     })
726    ///     .finish()?;
727    /// ```
728    pub fn body_fixed(
729        mut self,
730        len: usize,
731        f: impl FnOnce(&mut [u8]),
732    ) -> RequestBuilder<'a, Ready> {
733        seal_request_line(self.writer, &mut self.error);
734        // Write exact Content-Length (size known upfront).
735        write_body_header(&mut self.writer.write_buf, len, &mut self.error);
736        if self.error.is_some() {
737            return RequestBuilder {
738                writer: self.writer,
739                has_query: self.has_query,
740                error: self.error,
741                _phase: PhantomData,
742            };
743        }
744        // Reserve `len` bytes in the WriteBuf and let the closure write.
745        let buf = &mut self.writer.write_buf;
746        if len > buf.tailroom() {
747            self.error = Some(RestError::RequestTooLarge {
748                capacity: buf.len() + buf.tailroom(),
749            });
750        } else {
751            let start = buf.len();
752            // Extend the buffer with zeros, then give the closure mutable access.
753            buf.extend_zeroed(len);
754            let data = buf.data_mut();
755            f(&mut data[start..start + len]);
756        }
757        RequestBuilder {
758            writer: self.writer,
759            has_query: self.has_query,
760            error: self.error,
761            _phase: PhantomData,
762        }
763    }
764
765    /// Finish building. Returns the assembled request bytes.
766    pub fn finish(mut self) -> Result<Request<'a>, RestError> {
767        seal_request_line(self.writer, &mut self.error);
768        checked_append(&mut self.writer.write_buf, b"\r\n", &mut self.error);
769        if let Some(e) = self.error {
770            return Err(e);
771        }
772        Ok(Request {
773            data: self.writer.write_buf.data(),
774        })
775    }
776}
777
778// =========================================================================
779// Headers phase
780// =========================================================================
781
782impl<'a> RequestBuilder<'a, Headers> {
783    /// Add a request header.
784    pub fn header(mut self, name: &str, value: &str) -> Self {
785        self.append_header(name, value);
786        self
787    }
788
789    /// Set the request body. Transitions to the ready phase.
790    pub fn body(mut self, body: &[u8]) -> RequestBuilder<'a, Ready> {
791        write_body(&mut self.writer.write_buf, body, &mut self.error);
792        RequestBuilder {
793            writer: self.writer,
794            has_query: self.has_query,
795            error: self.error,
796            _phase: PhantomData,
797        }
798    }
799
800    /// Write the body directly into the buffer via a closure.
801    ///
802    /// Same as [`RequestBuilder<Query>::body_writer`] — see its docs.
803    pub fn body_writer<F, E>(mut self, f: F) -> RequestBuilder<'a, Ready>
804    where
805        F: FnOnce(&mut BodyWriter<'_>) -> Result<(), E>,
806        E: Into<Box<dyn std::error::Error + Send + Sync>>,
807    {
808        if self.error.is_some() {
809            return RequestBuilder {
810                writer: self.writer,
811                has_query: self.has_query,
812                error: self.error,
813                _phase: PhantomData,
814            };
815        }
816        let num_offset =
817            write_content_length_placeholder(&mut self.writer.write_buf, &mut self.error);
818        if self.error.is_some() {
819            return RequestBuilder {
820                writer: self.writer,
821                has_query: self.has_query,
822                error: self.error,
823                _phase: PhantomData,
824            };
825        }
826        let body_len = {
827            let mut bw = BodyWriter::new(&mut self.writer.write_buf);
828            if let Err(e) = f(&mut bw) {
829                // If the buffer is full, this is a capacity issue.
830                self.error = Some(if self.writer.write_buf.tailroom() == 0 {
831                    RestError::RequestTooLarge {
832                        capacity: self.writer.write_buf.len() + self.writer.write_buf.tailroom(),
833                    }
834                } else {
835                    RestError::Io(std::io::Error::other(e))
836                });
837                0
838            } else {
839                bw.written()
840            }
841        }; // bw dropped here, releasing write_buf borrow
842        if self.error.is_none() {
843            backfill_content_length(&mut self.writer.write_buf, num_offset, body_len);
844        }
845        RequestBuilder {
846            writer: self.writer,
847            has_query: self.has_query,
848            error: self.error,
849            _phase: PhantomData,
850        }
851    }
852
853    /// Write a fixed-size body via closure with direct `&mut [u8]` access.
854    ///
855    /// Same as [`RequestBuilder<Query>::body_fixed`] — see its docs.
856    pub fn body_fixed(
857        mut self,
858        len: usize,
859        f: impl FnOnce(&mut [u8]),
860    ) -> RequestBuilder<'a, Ready> {
861        write_body_header(&mut self.writer.write_buf, len, &mut self.error);
862        if self.error.is_some() {
863            return RequestBuilder {
864                writer: self.writer,
865                has_query: self.has_query,
866                error: self.error,
867                _phase: PhantomData,
868            };
869        }
870        let buf = &mut self.writer.write_buf;
871        if len > buf.tailroom() {
872            self.error = Some(RestError::RequestTooLarge {
873                capacity: buf.len() + buf.tailroom(),
874            });
875        } else {
876            let start = buf.len();
877            buf.extend_zeroed(len);
878            let data = buf.data_mut();
879            f(&mut data[start..start + len]);
880        }
881        RequestBuilder {
882            writer: self.writer,
883            has_query: self.has_query,
884            error: self.error,
885            _phase: PhantomData,
886        }
887    }
888
889    /// Finish building. Returns the assembled request bytes.
890    pub fn finish(mut self) -> Result<Request<'a>, RestError> {
891        checked_append(&mut self.writer.write_buf, b"\r\n", &mut self.error);
892        if let Some(e) = self.error {
893            return Err(e);
894        }
895        Ok(Request {
896            data: self.writer.write_buf.data(),
897        })
898    }
899
900    fn append_header(&mut self, name: &str, value: &str) {
901        if self.error.is_some() {
902            return;
903        }
904        if has_crlf(name) || has_crlf(value) {
905            self.error = Some(RestError::CrlfInjection);
906            return;
907        }
908        checked_append(&mut self.writer.write_buf, name.as_bytes(), &mut self.error);
909        checked_append(&mut self.writer.write_buf, b": ", &mut self.error);
910        checked_append(
911            &mut self.writer.write_buf,
912            value.as_bytes(),
913            &mut self.error,
914        );
915        checked_append(&mut self.writer.write_buf, b"\r\n", &mut self.error);
916    }
917}
918
919// =========================================================================
920// Ready phase
921// =========================================================================
922
923impl<'a> RequestBuilder<'a, Ready> {
924    /// Finish building. Returns the assembled request bytes.
925    pub fn finish(self) -> Result<Request<'a>, RestError> {
926        if let Some(e) = self.error {
927            return Err(e);
928        }
929        Ok(Request {
930            data: self.writer.write_buf.data(),
931        })
932    }
933}