HTTP files (.httf)
grpctestify runs three families of test file. A .gctf calls a gRPC method; a .httf makes an HTTP request; a .apif may do both in one chain. Everything around the call is the same in all three: the sections, the assertions, EXTRACT, chains, DATASET, OPTIONS, the reporters and the workbench.
What a file looks like
--- ADDRESS ---
https://api.example.com
--- ENDPOINT ---
POST /v1/users
--- REQUEST_HEADERS ---
authorization: Bearer {{TOKEN}}
--- REQUEST ---
{"name": "Ada"}
--- ASSERTS ---
@status() == 201
.name == "Ada"The call
ENDPOINT carries the method and the path — the same section a .gctf uses to say what is called:
--- ENDPOINT ---
POST /v1/users- Any method is accepted, including one this tool has never heard of (
PROPFIND,PURGE). - A path is joined to
ADDRESS; an absolute url is used as written and the address is not consulted. - An address without a scheme is dialled over
http://. {{variables}}are substituted in the path, so a step can call what an earlier step extracted.
A chain splits on ENDPOINT, as it does for gRPC, and a step without its own ADDRESS keeps the one the chain started with:
--- ADDRESS ---
https://api.example.com
--- ENDPOINT ---
POST /v1/users
--- REQUEST ---
{"name": "Ada"}
--- EXTRACT ---
user = .id
--- ENDPOINT ---
GET /v1/users/{{user}}
--- ASSERTS ---
.name == "Ada"The body
REQUEST is sent as written. JSON is the common case; anything else is a body too:
--- ENDPOINT ---
POST /submit
--- REQUEST ---
name=Ada&age=36The content-type is the one REQUEST_HEADERS names. When the file names none, it is inferred from the body: JSON, form-encoded, XML, or text/plain. A file with no REQUEST section sends no body at all, which is what most GETs and DELETEs want.
What must come back
The status is checked the way a .gctf checks one: in ASSERTS, beside everything else about the answer. RESPONSE compares the body.
--- ENDPOINT ---
DELETE /v1/users/7
--- ASSERTS ---
@status() == 204- A status assertion alone is a complete test — a
DELETEthat returns nothing has nothing else to compare. RESPONSEtakes the same inline options a.gctfone does:partial,tolerance,unordered_arrays,redact.- A response body that is not JSON is compared as the text it is.
ASSERTS reads the body, @header("…") reads the response headers and @status() reads the code:
--- ENDPOINT ---
GET /v1/users/7
--- ASSERTS ---
@status() == 200
@header("content-type") | test("application/json")
.name == "Ada"There is no ERROR section here: an HTTP call that returns 404 or 500 is a response that arrived with that status, so it is a RESPONSE for the body and @status() for the code. A status written into the header — --- RESPONSE 404 --- — is not read, and check says where it belongs.
What an HTTP file does not have
PROTO— there are no descriptors to load.TLS—https://in the address is the transport security.BENCH— the load runner is gRPC-shaped today.- The four RPC shapes: a request and a response, not a stream.
OPTIONS keeps timeout, retry, retry_delay and no_retry; protocol and compression belong to the gRPC family. Retries work the way they do for gRPC, and out of one budget: --retry, OPTIONS.retry and #[retry(N)] say how many further attempts a test gets, retry_delay how long to wait between them, and no_retry outranks all three. A call whose answer never arrived is sent again, and so is one whose @status() assertion failed against a 429, 502, 503 or 504. Nothing else is: a test that asked for 503 and failed a body check, or one that failed on the body alone, has its answer already — dialling again would change nothing.
Redirects
A 3xx is the answer the server gave, so the test sees it: @status() == 302 holds and @header("location") reads where it points. A file that wants the page behind the redirect says so in OPTIONS:
--- ENDPOINT ---
GET /old-path
--- OPTIONS ---
follow_redirects: true
--- ASSERTS ---
@status() == 200
@header(":url") | endswith("/new-path")follow_redirects defaults to false; with it on, @header(":url") carries the address the call landed on.
Running one
The same commands, without a flag to say which family:
grpctestify run api/ # both families, in one run
grpctestify check api/users.httf
grpctestify fmt --write api/
grpctestify play --dir . # the workbench opens bothOne request without a file, the way call makes a gRPC one:
grpctestify call -e 'GET /v1/users' --address https://api.example.com -i
grpctestify call -e 'POST /v1/users' --address https://api.example.com \
-H 'authorization: Bearer t0ken' -d '{"name": "Ada"}'The body is printed whatever the status. -f/--fail makes a 4xx or 5xx exit non-zero (22, as curl does); -S/--show-error says which status came back even when -s would otherwise keep quiet. -L/--location follows redirects, -i prints the response headers before the body and -D/--dump-header writes the status line and those headers to a file:
grpctestify call -e 'GET /old-path' --address https://api.example.com -L -D headers.txtThe flags that configure a gRPC dial — --insecure, --plaintext, --tls-ca, --tls-cert, --tls-key, --protocol — are refused on an HTTP call rather than silently ignored.
In the workbench an HTTP file gets a method-and-path control where a .gctf gets the method picker, its status badge reads 200 OK, and Copy as curl writes the same call as a command line. A curl command pasted into the import panel fills the request the same way a grpcurl one does.