Skip to main content

Request

Struct Request 

Source
pub struct Request {
Show 16 fields pub name: Option<String>, pub method: Method, pub url: String, pub headers: Vec<(String, String)>, pub query: Vec<(String, String)>, pub body: Option<String>, pub json: Option<Value>, pub body_file: Option<String>, pub form: Vec<(String, String)>, pub multipart: Vec<MultipartPart>, pub auth: Option<Auth>, pub assertions: Option<Assertions>, pub pre_request: Option<String>, pub post_request: Option<String>, pub capture: Option<Captures>, pub retry: Option<RetryConfig>,
}
Expand description

A single request, as described by one YAML file.

The on-disk shape is the contract other Sendra features build on:

name: Get user
method: GET
url: https://api.example.com/users/1
headers:
  Accept: application/json
body: null
assertions:
  status: 200

Everything but method and url is optional.

Headers are a Vec of pairs, not a map — matching Response::headers and for the same reason: HTTP allows a header name to repeat (multiple Set-Cookie-shaped headers, repeated X-Forwarded-For values), and a map cannot represent that. Order is preserved exactly as written in the file.

A standard YAML mapping still cannot have two keys with the same name, so writing a repeated header names it once with a list of values instead of a scalar:

headers:
  Accept: application/json    # scalar: one header
  X-Forwarded-For:            # list: one header per entry, in order
    - 1.2.3.4
    - 5.6.7.8

Two entries with the same name and the same value are accepted rather than rejected: Sendra’s stance elsewhere is to reject ambiguity, not redundancy, and a client is allowed to send the same header twice even when doing so is pointless.

Eq is deliberately absent where PartialEq is derived: an expected JSON value in an Assertions block can be a float, and JSON floats are not Eq. Nothing keys a map on a request, so the bound was never load-bearing.

Fields§

§name: Option<String>§method: Method§url: String§headers: Vec<(String, String)>§query: Vec<(String, String)>

Query parameters, merged onto whatever url already has and percent-encoded properly — the alternative to hand-building a query string inside url itself, where a value containing a space, &, = or non-ASCII character has to be encoded by hand or the request silently means something different than intended.

url: https://api.example.com/search
query:
  q: coffee & tea      # -> q=coffee+%26+tea
  tag:                 # list: one `tag=` per entry, in order
    - hot
    - iced

Request::resolve_query merges this onto url’s own query string (if it has one) with query winning: a key present in both is sent only with the value(s) from here, not the URL’s. query is the more structured, explicit source, so a key repeated between the two is far more likely to be a stale copy left in url than a deliberately duplicated value.

Deserialized the same way headers is — a value may be a scalar or, for a repeated key (?tag=hot&tag=iced), a list of scalars — since a repeated query parameter is the same shape of problem a repeated header already had a good answer for. An unquoted number or boolean is coerced to its string form rather than rejected, matching header values.

§body: Option<String>

Raw body, sent verbatim.

One of five ways to specify a body — body, json, body_file, form or multipart — and a request may set at most one of them; Request::validate rejects any other combination at parse time. By the time a pre_request script or send_prepared sees a request, whichever of the five was set has already been resolved down to this field by Request::resolve_body — see there for exactly what each one becomes.

§json: Option<Value>

A body given as YAML — a mapping, a list, a string, whatever value — serialized to JSON and sent as application/json.

json:
  name: ada
  roles: [admin, user]

Request::resolve_body sets Content-Type: application/json only when the request has not already set that header itself — an explicit header always wins. See Request::body for how this relates to the other four ways of specifying a body.

§body_file: Option<String>

A body read from a file, verbatim, sent exactly as body would be.

body_file: ./payload.json

The path is resolved relative to the request file’s own directory, not the process’s current working directory — see Request::resolve_body for why. Unlike json and form, no Content-Type is set automatically: Sendra cannot know what an arbitrary file contains, so a request using this field is responsible for its own headers: if the server needs one. The file’s content must be valid UTF-8 — see the module docs.

§form: Vec<(String, String)>

A body given as name/value pairs, URL-encoded and sent as application/x-www-form-urlencoded — the same encoding an HTML form submission uses.

form:
  username: ada
  remember_me: "true"

A plain YAML mapping cannot repeat a key, so unlike headers this has no list form for a repeated field name — nothing in Sendra has needed one yet. See Request::resolve_body for the Content-Type rule, which matches json’s.

Deserialized the same way headers is — a repeated field name is written as a list rather than rejected as a duplicate key — since a form field repeating (an HTML multi-select, say) is the same shape of problem a repeated header already had a good answer for.

§multipart: Vec<MultipartPart>

A body given as named parts, each either inline text or a file, sent as multipart/form-data.

multipart:
  - name: description
    value: a photo of my cat
  - name: photo
    path: ./cat.jpg

Each part is exactly one of a text part (value) or a file part (path, resolved the same way body_file is) — Request::validate rejects a part with both or neither. See the module docs for why a file part’s content must be valid UTF-8 in this version.

§auth: Option<Auth>

How to authenticate this request: bearer token, basic credentials or a static API key, resolved to a plain header (and, for api_key in query form, a query parameter) before anything else sees it.

auth:
  bearer: {{token}}

# or

auth:
  basic:
    user: {{username}}
    pass: {{password}}

# or

auth:
  api_key:
    in: header          # or: query
    name: X-API-Key     # or a query param name
    value: {{api_key}}

Exactly one of bearer/basic/api_key may be set — Request::validate rejects any other combination, the same shape as the five body fields above. A request may not set auth and an explicit header (or, for api_key in query form, query parameter) of the same name it would itself set: both trying to control the same header (or parameter) is far more likely a mistake than a deliberate layering (unlike, say, a config default header and a request header, where “the request wins” is a sensible answer), so validate rejects the combination rather than silently picking one.

Request::resolve_auth turns this into the final header (or query parameter) and clears the field, following the same “scripts see the final resolved form” precedent as Request::resolve_body: a pre_request script reads or overrides request.headers["Authorization"] (or any other header api_key set) like any other header, with no separate request.auth API.

§assertions: Option<Assertions>

Declarative checks on the response, evaluated by Assertions::evaluate once it arrives.

None — no assertions: key at all — is not the same as an empty block, and both are kept distinct on the way back out to YAML. Neither changes how the request is sent: assertions are read after the response, never before it, and they do not decide the process exit code. See the module docs on assertions.

§pre_request: Option<String>

A script run against this request just before it is sent, as inline Rhai source.

Written as a YAML block scalar, which is what a multiline script needs and the reason the file format is YAML rather than JSON or TOML:

pre_request: |
  request.headers["X-Request-Id"] = "abc-123";

It runs after environment substitution and after the config is applied, as the final mutation step before the wire. Its own source is never substituted — a {{var}} inside a script is just those characters. See the script module for both decisions and for what the script can see.

§post_request: Option<String>

A script run against the response, before assertions are evaluated.

post_request: |
  if response.status != 201 {
    throw "expected 201, got " + response.status;
  }

throw is how it reports a failure. Like an assertion, that failure is visible in the output and decides sendra test’s verdict without changing sendra run’s exit code. Compiled before the request is sent, so a syntax error here stops the request rather than being discovered after it.

§capture: Option<Captures>

Values to pull out of the response and hand to the requests after this one, as variable name to JSON path:

capture:
  auth_token: $.token
  user_id: $.user.id

Each name becomes usable as {{name}} in every request after this one in file order, within the same sendra run or sendra test invocation — nothing is written to disk and a fresh process starts with nothing captured.

None — no capture: key at all — is kept distinct from an empty block on the way back out to YAML, the same way an assertions block is. Neither changes how this request is sent: a capture is read after the response, never before it. See the capture module for what a path may select and for what happens when one does not match.

The block is not substituted. A {{var}} in a capture path or name stays those characters; see Environment::apply.

§retry: Option<RetryConfig>

Retry this request on a true failure — no response at all: a DNS, connection, TLS or timeout error — up to count additional attempts, waiting delay_ms (default 0, no wait) between each.

retry:
  count: 2
  delay_ms: 200

Only a failure to get any response triggers a retry. A 4xx/5xx is still a response — send_prepared returns it as Ok, not Err — so it is never retried by this field; retrying on a specific status is a separate, more advanced feature this does not attempt. Simple, fixed backoff: no exponential delay or jitter.

Only the final attempt’s outcome is reported. A request that fails twice and then succeeds is reported as a plain, ordinary success — the two failed attempts before it are never counted toward sendra test’s summary or either subcommand’s exit code, though each retry is logged to stderr for visibility. A request that exhausts every attempt is reported as the one failure it always would have been, with no separate record of the attempts that came before it.

None — no retry: key at all — means a request is sent once and whatever happens is final, exactly as it was before this field existed.

Implementations§

Source§

impl Request

Source

pub fn resolve_query(&self) -> Result<Request, SendraError>

Merge query onto url’s own query string, percent-encoded properly, returning a request whose url is the final string that goes on the wire and whose query is empty.

Called right after resolve_auth — which, for an auth.api_key in query form, has already appended its name/value pair onto query so it merges through this exact mechanism rather than a separate one — and before resolve_body, the config, or a pre_request script ever see the request. A pre_request script therefore sees query parameters (including any from auth.api_key) already merged into request.url, not a separate map, for consistency with resolve_body’s “scripts see the final resolved form” precedent.

A request with an empty query is returned with url untouched — not even reparsed — so a url-only request behaves exactly as it always has, including one whose url would not itself parse as a valid reqwest::Url (which today is only ever caught by reqwest itself, at send time).

Uses reqwest::Url’s own query-pair APIs — already a dependency — rather than string concatenation, so a value containing a space, &, = or non-ASCII character is encoded correctly rather than however it happened to be typed.

Source

pub fn resolve_body(&self, base_dir: &Path) -> Result<Request, SendraError>

Resolve whichever of body/json/body_file/form/multipart was set into the final body string that goes on the wire, setting Content-Type when the field implies one and the request has not already set that header itself.

Called once, after environment substitution and before the config is applied or a pre_request script runs — so both see a plain body string regardless of which field produced it, the same way they already see a request whose {{var}}s have been resolved. json, body_file, form and multipart are cleared on the way out; body is the only body field left on the result.

base_dir is where body_file and a multipart part’s path resolve relative to: the directory containing the request’s own YAML file, not the process’s current working directory. A request file is something a user can run from anywhere — sendra run requests/create-user.yaml from a repository root — and body_file: ./payload.json written inside create-user.yaml obviously means the file beside it, not one resolved against whatever directory the command happened to be typed from.

json, form and body_file’s path were already substituted by Environment::apply before this runs. body_file’s file content is deliberately not substituted — it is external content Sendra reads, not a value written in the request file, and substitution has never reached outside the document; see the environment module docs.

File content — for body_file and a multipart file part alike — is read as UTF-8 text; a file that is not valid UTF-8 is SendraError::BodyFileIo. Sendra’s bodies are text throughout, the same way a Response’s is, and true binary uploads are out of scope for this version.

Source

pub fn resolve_auth(&self) -> Result<Request, SendraError>

Resolve auth into the header (bearer/basic/an api_key in header form) or query parameter (an api_key in query form) that goes on the wire, clearing auth on the way out.

Called right after environment substitution and before resolve_query, resolve_body, the config, or a pre_request script ever see the request. It runs before resolve_query specifically so that an auth.api_key in query form can hand its name/value pair to query and let resolve_query do the actual merging onto url — the same percent-encoding and “the more structured source wins on a name collision with the URL’s own query string” rule an ordinary query: entry gets, rather than a second, parallel implementation. A pre_request script therefore sees a plain Authorization (or other) header like any other, with no separate request.auth API — and, for the query form, sees the parameter already merged into request.url by the time resolve_query has also run.

Request::validate has already rejected a request that sets auth alongside an explicit header or query parameter of the same name it would itself set, so this always adds the header/parameter rather than needing config::insert_if_absent’s suppression rule.

Source

pub async fn resolve_oauth( &self, client: &HttpClient, cache: &OAuthTokenCache, ) -> Result<Request, SendraError>

Acquire an OAuth token for auth.oauth, collapsing it into the exact bearer form resolve_auth already knows how to turn into an Authorization header — so oauth is a front end for the bearer case, not a second header-setting implementation. A request whose auth is None, or whose auth.oauth is None, is returned unchanged; there is nothing to acquire.

Called once, before resolve_auth, from the request-resolution pipeline in sendra-cli — the one step in that pipeline that needs the shared HttpClient and an .await, since acquiring a token is a real HTTP call to token_url. See crate::oauth for the cache this reads and writes, the retry-vs-fail-fast decision for a broken config, and the expiry margin.

Source§

impl Request

Source

pub fn from_yaml_str(yaml: &str) -> Result<Self, SendraError>

Parse a request from a YAML string.

Source

pub fn from_path(path: impl AsRef<Path>) -> Result<Self, SendraError>

Read and parse a request from a YAML file on disk.

Source

pub fn label(&self) -> String

Display label: the name field if present, else METHOD url.

Source

pub fn header(&self, name: &str) -> Option<&str>

The first header with exactly this name, if any.

A convenience for callers that know (or only care about) at most one occurrence; a header that may legitimately repeat should read .headers directly rather than lose every occurrence but the first.

Trait Implementations§

Source§

impl Clone for Request

Source§

fn clone(&self) -> Request

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Request

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for Request

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl PartialEq for Request

Source§

fn eq(&self, other: &Request) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for Request

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Request

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more