Upgrade safely
1
Validate with the new version
hitspec validate reports every file that no longer loads, with
file:line:col and a suggested fix, and prints warnings such as unknown
directives. --strict turns the warnings into errors and also checks that
every {{variable}} is defined. See validate.2
Run the suite once locally
Run your tests with the new version and read each failure against the
lists below before you touch CI.
3
Pin the version in CI
Upgrade CI on purpose, not as a side effect of a new release. With the
GitHub Action, set With Go, install an exact version:
version::go install github.com/abdul-hamid-achik/hitspec/v2/cmd/hitspec@v2.24.0.2.24.0
reporters and outputDir in hitspec.yaml used to be read and ignored. They
now apply in hitspec run. Nothing changes if your config does not set them,
or sets only reporters: [console].
reporters and outputDir
- An unknown reporter name exits 3. Valid names:
console,json,junit,tap,html. - Two or more reporters without
outputDirexit 3: they would all write to stdout. SetoutputDiror keep one reporter. - One reporter other than
consolewithoutoutputDirreplaces the console report on stdout (reporters: [json]is-o json). - With
outputDir, reports are written there ashitspec-results.json,hitspec-junit.xml,hitspec-results.tapandhitspec-report.html, relative to the config file. If no report goes to stdout, failures are listed on stderr as with--quiet. -o,--output-file,HITSPEC_OUTPUTandHITSPEC_OUTPUT_FILEstill win and skip both keys. See Configuration.
Windows
- A connection the server resets before any response exits 4 (network error), as on Linux and macOS. It used to exit 1 when Winsock reported the reset.
hitspec studio
- The stress form has a new last field,
profile, for a saved stress profile. The other fields keep their order. - Search re-parses a file only when its modification time or size changed, or when studio wrote it.
2.23.2
hitspec run --stressprints no color codes when stdout is not a terminal, as before 2.23.1. Only a check that expected escape codes in a file or pipe is affected.- Go library:
output.WithNoColor(true)turns colors off for that formatter only, not for the whole process.
2.23.1
hitspec run --metrics ...without--stress, or withHITSPEC_METRICSset, prints a warning on stderr. It still exits 0. Add--stressor unset the variable for normal runs.- studio and
serverefuse a__snapshots__directory or snapshot file that a symlink points to outside the workspace.hitspec runis unchanged. - Stress metrics (JSON, Prometheus, DataDog) now include
status_codes; before, they were always empty. hitspec export --execruns throughshinstead ofbash.
2.23.0
2.23.0 runs the whole test suite on Windows and fixes what that found. It also applies twohitspec.yaml keys that used to be ignored. You only need to
change a .http file if it relied on one of the old behaviors below.
Exit codes
- A busy port exits 3 in every command.
hitspec mockandhitspec recordused to exit 1.hitspec run --stress --metrics prometheusused to print the error and run the whole load test without metrics. It now stops before sending a request. See Exit codes.
Settings that now apply
noColor: trueinhitspec.yamlturns colors off inhitspec run.--no-color=falseorHITSPEC_NO_COLOR=falseturns them back on.- With
verbose: true, stderr starts with theconfig: <path>line, as with-v.--verbose=0turns both off.
studio, serve and the desktop app
- A
schemafile that a symlink points to outside the workspace is refused, as body files already were.hitspec runis unchanged. - Apply on a saved stress profile in the desktop app now sends the profile, so its thresholds are checked. A run can report failed thresholds where it used to ignore them.
- New JSON fields, omitted when empty:
durationon the stress status andstress_updateevents,warningson history results, and an optionalprofileonPOST /api/v1/stress/start. Strict schema validators must allow them. - In
hitspec studio,enterin search selects the request it found, and unsaved edits survive the file being deleted on disk (ctrl+srecreates it).
Windows
- Hooks and
>>>shellblocks run through Git for Windows’shwhen it is installed. Without it they fall back tocmd /C, where POSIX syntax does not work. - A contract state handler such as
setup-states.shruns through thatsh. - Quoted globs (
hitspec run 'tests/**/*.http') work. - studio,
serveand the desktop app report paths with/, and so doeshitspec import -o. - The MCP server refuses
/x,\xandC:xpaths. - On every platform, certificate and key errors show the path in plain quotes, without doubled backslashes.
2.22.0
Most of 2.22.0 adds things, but some changes show up as a different exit code, a request sent differently, or a check that used to be switched off. None of them needs an edit to a.http file unless it relied on one of the old
behaviors below.
Exit codes
- Network errors exit 4. A request that gets no response because the
connection failed (refused, DNS, TLS or certificate errors, a reset before
any response, a timeout before connecting, an OAuth2 token endpoint or a
@waitFortarget that cannot be reached) used to exit 1. If a CI step accepts only 0 and 1, treat 4 as a failure too. A timeout after connecting and assertion failures are still 1. See Exit codes. - Ctrl+C and SIGTERM exit 130, after printing the finished requests and
writing
--output-file. SIGTERM used to read as 143. - Command-line mistakes exit 64: an unknown flag or command, a bad flag
value, a missing required flag, the wrong number of arguments, and
--dry-runwith--stress(which used to run the load test). They exited 1. - More configuration errors exit 3 before any request is sent: a missing
--env-file, a CA, certificate, key or proxy that cannot be loaded, and a brokenhitspec.yamlinvalidate,studio,serveandcontract(studio and serve used to fall back to the defaults silently). --dry-runparses. A file that does not load exits 2, and filters that match exit 0. The plan is always text:-oand--output-fileare ignored.
Which settings apply
hitspec.yamlis found by walking up from the working directory, stopping after the first directory with.git, so a config in a parent directory of your tests now applies.hitspec run -vprints the file it used. Unknown keys print a warning;hitspec validate --strictturns them into errors.verbose: trueinhitspec.yamlnow works (it is-v);--verbose=0turns it off.
Requests sent differently
- Query lines (
? name = value) keep their order and duplicates and encode a space as%20. They used to be sorted, collapsed and encoded with+. - Illegal URL characters (spaces, quotes, non-ASCII) are percent-encoded instead of being sent raw.
- Form blocks (
& name = value) keep the spaces in values. - A body file that cannot be read (
< ./file, a multipart file) fails the request. The literal text used to be sent. - Unnamed multipart file parts are sent as field
file. Name them withfile avatar @./me.png. @waitForuses the run’s client: proxy, CA,--insecure, default headers andfollowRedirects: falseapply to it.- Cookies are only kept between requests with
--cookies(orcookies: true).
Reports and file selection
- Failure output shows the request, the response status and a body excerpt.
-vprintsResponse: 200 OKinstead ofStatus: 200;-vvadds headers and-vvvbodies. - More header names are redacted (anything with
token,secret,passwordorapi-key), in every format including HTML. - JSON, JUnit and TAP carry new fields such as
warnings. JUnit states each failed assertion once, and TAP escapes backslashes and newlines. - Directories are searched without
.git,node_modulesandvendor, and a file reached twice runs once.
mock, record, stress and metrics
mockandrecordlisten on127.0.0.1; pass--host 0.0.0.0for Docker. A busy port is an error.- The Prometheus endpoint listens on
127.0.0.1; pass--metrics-host 0.0.0.0when another machine or container scrapes it. mockanswers a wrong method with 405 and serves HEAD for GET routes; CORS needs--cors.recordredacts tokens, passwords, secrets, API keys andSet-Cookiein recordings (--no-redactkeeps them).- Stress results count requests cut off at the end as incomplete, and
assertion failures have their own
assertions<N%threshold, sorpsanderrorsthresholds can move. errors<0anderrors: "0%"are enforced. They used to switch the check off, so a run with errors passed; now one failed request fails it.- With
--profile, explicit flags win, and profile thresholds keep their operator (rps: ">=100").
serve, studio and the desktop app
--read-onlylets you run requests, stress, mock, record, contract verification and import; it only refuses writes to the workspace and settings.- Body files outside the workspace are refused in
serve,studioand the desktop app, including in stress and contract runs. - Starting mock or record on a busy port fails (409 from
serve), and a stress run that cannot start reportserror. PUT /api/v1/configapplies a key that is present even when it is0or"". Clients that sent those to mean “unchanged” must leave the key out.- In
hitspec studio,enterno longer submits forms (uses, orctrl+swhile editing),rruns the editor’s unsaved text,ctrl+ccancels a run and asks before discarding edits, copy-as fills in{{variables}}(secrets included), and the mouse is captured only with--mouse.
2.21.0
Files that no longer load (exit code 2)
-
Misspelled operators. They were silently treated as
==.expect body.name contians "x"now fails withunknown operator "contians" (did you mean "contains"?). -
An operator with no value, or with two.
expect status 200 201is an error. To accept several values, usein: -
Blocks that do not belong to a request. A
>>>,>>>capture,>>>dbor>>>shellblock above its request, a block with an unknown type, or a block without its closing<<<was dropped without a word. Move the block below its request, fix the type, or close it. -
>>>db <connection>. That form never existed, so its checks never ran. Set the connection with# @dband put the SQL and the column checks inside the block (see Database Testing). -
Unsupported
jsonpathfeatures (..,?()filters, slices, unions) are errors instead of silently matching nothing.
Assertions that now check what they say
-
null versus absent.
== nullneeds a JSONnull; a missing field no longer equals null. Use!existsto check that a field is absent. Value operators (==,>,contains, …) fail on a missing field; the negated ones (!=,!contains,!includes,!in) pass. -
Non-JSON bodies.
body.xon an HTML or plain-text response fails with “response body is not JSON” instead of comparing against nothing.application/problem+json, other+jsontypes andtext/jsoncount as JSON. -
Unquoted values are read whole.
expect body.name == Jane DoecomparesJane Doe; it used to compare onlyJane. A trailing# commentis no longer part of the value. -
jsonpath $.xsubjects are evaluated. They used to resolve to nothing, so!existsand!=passed whatever the response held. -
Objects and arrays compare by value, and numbers compare numerically:
-
lengthcounts characters, not bytes."café"has length 4; it was 5. -
contains,startsWith,endsWithandmatcheson a whole JSON body, an object or an array use its JSON text. -
Paths with
.#and#(...)work. The#used to start a comment and cut the path short. -
# @ifand# @unlessevaluate==,!=,!,&&and||. Before, any non-empty text was true, so# @if {{env}} == prodran the request in every environment. It is now skipped outside prod. -
Directives above the first request of a file without a
###line were dropped. Its@name,@tags,@skipand@dependsnow apply, so tag filters and dependencies can select different requests. -
>>>dbblocks written as bare SQL were dropped. They now run, and need--allow-db.
Settings that now apply
- hitspec.yaml in
hitspec run.timeout,retries,retryDelay,maxRedirects,parallel,concurrencyandbailwere overridden by the flag defaults. A yamltimeout: 5000now fails requests slower than five seconds,retries: 3repeats failing requests, andbail: truestops at the first failure. HITSPEC_*=falseoverrides hitspec.yaml (HITSPEC_INSECURE,HITSPEC_PARALLEL,HITSPEC_BAIL). The order everywhere is: flag, thenHITSPEC_*variable, then hitspec.yaml, then the built-in default.maxRedirects: Nfollows N redirects. It followed N-1.
HTTP behavior
- Proxy variables are honored.
HTTP_PROXY,HTTPS_PROXYandNO_PROXYapply when no proxy is configured. On a runner with a proxy in its environment, requests now go through it; list hosts that must be reached directly inNO_PROXY. Loopback hosts are never proxied. - Credentials stay on their origin. On a redirect to another scheme, host
or port, the headers you set (
Authorization,Cookie,X-Api-Key, …) are not forwarded. A test that relied on the credential following the redirect gets a 401: point it at the final URL. Host:is sent as the request’s Host. It used to be ignored.- Compressed bodies are decoded. With an explicit
Accept-Encoding: gzipordeflate, assertions see the decoded body, not compressed bytes. - Repeated
Set-Cookieheaders are joined with,(it was;), like every other repeated header. - Response bodies over 100 MiB fail with “response body exceeds”.
- HTTP/2 is negotiated over TLS.
Other commands
hitspec diff. Skipped tests have their own status and no longer count as failures or regressions. A 0 ms baseline never trips--threshold.hitspec mcp serve. Saved requests run byhitspec_fetchno longer read the server’s environment variables; allow one with--allow-env NAME.--workspacerefuses your home directory, its parents and/unless you pass--allow-broad-workspace.
2.20.0
expect header X existspassed even when the header was missing. It now fails in that case.- A body cut short by a timeout (
@timeout,--timeout) counted as a success with a partial body. It is now a failure. Onlytext/event-stream(SSE) responses may end at the deadline. - Files that fail to load (a syntax error, a duplicate
@name, a circular@depends) were skipped and the run could still exit 0.hitspec runnow exits 2.--nameand--tagsfilters that match nothing exit 64. See Exit Codes. @authwith an unknown type or the wrong number of arguments is a parse error.- The default environment. Without
--envorHITSPEC_ENV,hitspec runuses hitspec.yamldefaultEnvironment(it always useddev), so variables such as{{baseUrl}}can resolve differently. - Header values are sent verbatim. Quotes and text after
#are kept:If-None-Match: "abc"sends the quotes. - Captured values are inserted literally. A
{{...}}inside a captured value is no longer expanded.