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: 200Everything 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.8Two 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
- icedRequest::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.jsonThe 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.jpgEach 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.idEach 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: 200Only 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
impl Request
Sourcepub fn resolve_query(&self) -> Result<Request, SendraError>
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.
Sourcepub fn resolve_body(&self, base_dir: &Path) -> Result<Request, SendraError>
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.
Sourcepub fn resolve_auth(&self) -> Result<Request, SendraError>
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.
Sourcepub async fn resolve_oauth(
&self,
client: &HttpClient,
cache: &OAuthTokenCache,
) -> Result<Request, SendraError>
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
impl Request
Sourcepub fn from_yaml_str(yaml: &str) -> Result<Self, SendraError>
pub fn from_yaml_str(yaml: &str) -> Result<Self, SendraError>
Parse a request from a YAML string.
Sourcepub fn from_path(path: impl AsRef<Path>) -> Result<Self, SendraError>
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, SendraError>
Read and parse a request from a YAML file on disk.
Sourcepub fn header(&self, name: &str) -> Option<&str>
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.