|
| 1 | +--- |
| 2 | +title: Hurl 7.1.0, the Pretty Edition |
| 3 | +layout: blog |
| 4 | +section: Blog |
| 5 | +permalink: /blog/:year/:month/:day/:title.html |
| 6 | +--- |
| 7 | + |
| 8 | +# {{ page.title }} |
| 9 | + |
| 10 | +<div class="blog-post-date">{{ page.date | date: "%b. %d, %Y" }}</div> |
| 11 | + |
| 12 | +<p> |
| 13 | +<picture> |
| 14 | + <source srcset="{{ '/assets/img/curl-plus-jq.avif' | prepend:site.baseurl }}" type="image/avif"> |
| 15 | + <source srcset="{{ '/assets/img/curl-plus-jq.webp' | prepend:site.baseurl }}" type="image/webp"> |
| 16 | + <source srcset="{{ '/assets/img/curl-plus-jq.png' | prepend:site.baseurl }}" type="image/png"> |
| 17 | + <img class="u-drop-shadow u-border" src="{{ '/assets/img/curl-plus-jq.png' | prepend:site.baseurl }}" width="100%" alt="Hurl 7.1.0, the pretty edition"/> |
| 18 | +</picture> |
| 19 | +</p> |
| 20 | + |
| 21 | +The Hurl team is thrilled to announce [Hurl 7.1.0] <picture><source srcset="{{ '/assets/img/emoji-rocket.avif' | prepend:site.baseurl }}" type="image/avif"><source srcset="{{ '/assets/img/emoji-rocket.webp' | prepend:site.baseurl }}" type="image/webp"><source srcset="{{ '/assets/img/emoji-rocket.png' | prepend:site.baseurl }}" type="image/png"><img class="emoji" src="{{ '/assets/img/emoji-rocket.png' | prepend:site.baseurl }}" width="20" height="20" alt="Partying face emoji"></picture> ! |
| 22 | + |
| 23 | +[Hurl] is a command line tool powered by [curl], that runs HTTP requests defined in a simple plain text format: |
| 24 | + |
| 25 | +```hurl |
| 26 | +GET https://example.org/api/tests/4567 |
| 27 | +HTTP 200 |
| 28 | +[Asserts] |
| 29 | +jsonpath "$.status" == "RUNNING" # Check the status code |
| 30 | +jsonpath "$.tests" count == 25 # Check the number of items |
| 31 | +jsonpath "$.id" matches /\d{4}/ # Check the format of the id |
| 32 | +
|
| 33 | +# Some tests on the HTTP layer: |
| 34 | +GET https://example.org |
| 35 | +HTTP 200 |
| 36 | +[Asserts] |
| 37 | +header "x-foo" contains "bar" |
| 38 | +certificate "Expire-Date" daysAfterNow > 15 |
| 39 | +ip == "2001:0db8:85a3:0000:0000:8a2e:0370:733" |
| 40 | +``` |
| 41 | + |
| 42 | +## What’s New in This Release |
| 43 | + |
| 44 | +- [JSON Response Automatic prettifying](#json-response-automatic-prettifying) |
| 45 | +- [New Predicates `isObject`, `isList`](#new-predicates-isobject-islist) |
| 46 | +- [New Supported curl options](#new-supported-curl-options) |
| 47 | +- [New Filters `utf8Decode`, `utf8Encode`](#new-filters-utf8decode-utf8encode) |
| 48 | + |
| 49 | +## JSON Response Automatic prettifying |
| 50 | + |
| 51 | +Hurl supports two running modes: |
| 52 | + |
| 53 | +- the "default" one |
| 54 | + |
| 55 | + hurl books.hurl |
| 56 | + |
| 57 | +- the test-oriented one |
| 58 | + |
| 59 | + hurl --test books.hurl |
| 60 | + |
| 61 | +In both cases, asserts and captures are run, the difference between these two modes is about what is written on standard output and standard error. |
| 62 | + |
| 63 | +With default mode, the last HTTP response is written on standard output, as received from the network. You can use this mode |
| 64 | +when you want to get data from a server, and you need a kind of workflow to get it. It's like curl, but it's easier to chain requests and pass |
| 65 | +data from response to request (like [OAuth], [CSRF] etc...). |
| 66 | + |
| 67 | +With test mode, no response is written on standard output, but the display is tweaked for a test run, with a succinct summary: |
| 68 | + |
| 69 | +``` |
| 70 | +$ hurl --test *.hurl |
| 71 | +... |
| 72 | +-------------------------------------------------------------------------------- |
| 73 | +Executed files: 100 |
| 74 | +Executed requests: 100 (1612.9/s) |
| 75 | +Succeeded files: 100 (100.0%) |
| 76 | +Failed files: 0 (0.0%) |
| 77 | +Duration: 62 ms (0h:0m:0s:62ms) |
| 78 | +``` |
| 79 | + |
| 80 | +Starting with Hurl 7.1.0, we've improved the default mode, when response is displayed on standard output. |
| 81 | +If the response is a JSON, we're displaying now a pretty, colored, indented version of it: |
| 82 | + |
| 83 | + |
| 84 | +```shell |
| 85 | +$ hurl books.hurl |
| 86 | +[1;39m{[0m |
| 87 | + [1;34m"store"[0m[1;39m:[0m [1;39m{[0m |
| 88 | + [1;34m"book"[0m[1;39m:[0m [1;39m[[0m |
| 89 | + [1;39m{[0m |
| 90 | + [1;34m"category"[0m[1;39m:[0m [0;32m"reference"[0m[1;39m,[0m |
| 91 | + [1;34m"author"[0m[1;39m:[0m [0;32m"Nigel Rees"[0m[1;39m,[0m |
| 92 | + [1;34m"title"[0m[1;39m:[0m [0;32m"Sayings of the Century"[0m[1;39m,[0m |
| 93 | + [1;34m"price"[0m[1;39m:[0m [0;36m8.95[0m |
| 94 | + [1;39m}[0m[1;39m,[0m |
| 95 | + [1;39m{[0m |
| 96 | + [1;34m"category"[0m[1;39m:[0m [0;32m"fiction"[0m[1;39m,[0m |
| 97 | + [1;34m"author"[0m[1;39m:[0m [0;32m"J. R. R. Tolkien"[0m[1;39m,[0m |
| 98 | + [1;34m"title"[0m[1;39m:[0m [0;32m"The Lord of the Rings"[0m[1;39m,[0m |
| 99 | + [1;34m"isbn"[0m[1;39m:[0m [0;32m"0-395-19395-8"[0m[1;39m,[0m |
| 100 | + [1;34m"price"[0m[1;39m:[0m [0;36m22.99[0m |
| 101 | + [1;39m}[0m |
| 102 | + [1;39m][0m[1;39m,[0m |
| 103 | + [1;34m"bicycle"[0m[1;39m:[0m [1;39m{[0m |
| 104 | + [1;34m"color"[0m[1;39m:[0m [0;32m"red"[0m[1;39m,[0m |
| 105 | + [1;34m"price"[0m[1;39m:[0m [0;36m399[0m |
| 106 | + [1;39m}[0m |
| 107 | + [1;39m}[0m |
| 108 | +[1;39m}[0m |
| 109 | +``` |
| 110 | +
|
| 111 | +Before Hurl 7.1.0, you can achieve the same result with piping to [jq](https://jqlang.org): |
| 112 | +
|
| 113 | +```shell |
| 114 | +$ hurl foo.hurl | jq |
| 115 | +``` |
| 116 | +
|
| 117 | +Now, you can just run: |
| 118 | +
|
| 119 | +```shell |
| 120 | +$ hurl foo.hurl |
| 121 | +``` |
| 122 | +
|
| 123 | +and the response will be pretty printed. This improvement adresses one of our top-voted issues |
| 124 | +since 2023, so we're very happy to have implemented it in this release! |
| 125 | +
|
| 126 | +<p> |
| 127 | +<picture> |
| 128 | + <source srcset="{{ '/assets/img/top-voted-issue.avif' | prepend:site.baseurl }}" type="image/avif"> |
| 129 | + <source srcset="{{ '/assets/img/top-voted-issue.webp' | prepend:site.baseurl }}" type="image/webp"> |
| 130 | + <source srcset="{{ '/assets/img/top-voted-issue.png' | prepend:site.baseurl }}" type="image/png"> |
| 131 | + <img class="u-drop-shadow u-border" src="{{ '/assets/img/top-voted-issue.png' | prepend:site.baseurl }}" width="100%" alt="Top-voted issue"/> |
| 132 | +</picture> |
| 133 | +</p> |
| 134 | +
|
| 135 | +### Under the hood |
| 136 | +
|
| 137 | +Prettifying JSON response is optimized to consume the fewest possible ressources, so there is no performance |
| 138 | +hits for a normal usage, even with big responses. |
| 139 | +
|
| 140 | +Some implementation details worth noting: |
| 141 | +
|
| 142 | +#### No Unicode escape transformation |
| 143 | +
|
| 144 | +In JSON, characters can be written directly using UTF-8 or using Unicode escapes. For instance, a string containing an emoji can be written like this: |
| 145 | +
|
| 146 | +```json |
| 147 | +{ |
| 148 | + "an emoji with UTF-8": "🏝️", |
| 149 | + "an emoji with Unicode escapes": "\uD83C\uDFDD" |
| 150 | +} |
| 151 | +``` |
| 152 | +
|
| 153 | +The two strings represent exactly the same Unicode char. With this input, different program will prettify |
| 154 | +JSON differently. Let's take [jq](https://jqlang.org), the de facto standard to manipulate JSON data, and [HTTPie](https://httpie.io), |
| 155 | +one of the best HTTP client. |
| 156 | +
|
| 157 | +jq, by default, will normalize Unicode escapes, rendering Unicode escapes to their UTF-8 representation: |
| 158 | +
|
| 159 | +```shell |
| 160 | +$ cat island.json | jq |
| 161 | +[1;39m{ |
| 162 | + [0m[1;34m"an emoji with UTF-8"[0m[1;39m: [0m[0;32m"🏝️"[0m[1;39m, |
| 163 | + [0m[1;34m"an emoji with Unicode escapes"[0m[1;39m: [0m[0;32m"🏝"[0m[1;39m |
| 164 | +[1;39m}[0m |
| 165 | +``` |
| 166 | +
|
| 167 | +HTTPie renders also Unicode escapes: |
| 168 | +
|
| 169 | +```shell |
| 170 | +$ http http://localhost:8000/island.json |
| 171 | +[34mHTTP[0m/[34m1.0[0m [34m200[0m [36mOK[0m |
| 172 | +[36mContent-Length[0m: 83 |
| 173 | +... |
| 174 | + |
| 175 | +{ |
| 176 | + [1;34m"an emoji with UTF-8"[0m: [33m"🏝️"[0m, |
| 177 | + [1;34m"an emoji with Unicode escapes"[0m: [33m"🏝"[0m |
| 178 | +} |
| 179 | +``` |
| 180 | +
|
| 181 | +Contrary to jq and HTTPie, Hurl will minimally prettify JSON, just coloring Unicode escapes: |
| 182 | +
|
| 183 | +```shell |
| 184 | +$ echo 'GET http://localhost:8000/island.json' | hurl |
| 185 | +[1;39m{[0m |
| 186 | + [1;34m"an emoji with UTF-8"[0m[1;39m:[0m [0;32m"🏝️"[0m[1;39m,[0m |
| 187 | + [1;34m"an emoji with Unicode escapes"[0m[1;39m:[0m [0;32m"\uD83C\uDFDD"[0m |
| 188 | +[1;39m}[0m |
| 189 | +``` |
| 190 | +
|
| 191 | +The idea is to add colors and indentations, but leave the input source "unchanged" otherwise. |
| 192 | +
|
| 193 | +### Numbers are left untouched |
| 194 | +
|
| 195 | +In JSON, float numbers can be represented in different ways, for instance 1,234 can be written `1234`, `1.234e3` or |
| 196 | +even `1.234E+3`. |
| 197 | +
|
| 198 | +Given this input: |
| 199 | +
|
| 200 | +``` |
| 201 | +{ |
| 202 | + "scientific_notation_positive": 1.23e4, |
| 203 | + "scientific_notation_negative": 6.02e-3, |
| 204 | + "scientific_uppercase_E": 9.81E+2 |
| 205 | +} |
| 206 | +``` |
| 207 | +
|
| 208 | +jq normalizes numbers, keeping fields order: `1.23e4` becomes `1.23E+4`, `6.02e-3` becomes `0.00602` and `9.81E+2` |
| 209 | +becomes `981`. |
| 210 | +
|
| 211 | +```shell |
| 212 | +$ cat numbers.json | jq |
| 213 | +[1;39m{ |
| 214 | + [0m[1;34m"scientific_notation_positive"[0m[1;39m: [0m1.23E+4[1;39m, |
| 215 | + [0m[1;34m"scientific_notation_negative"[0m[1;39m: [0m0.00602[1;39m, |
| 216 | + [0m[1;34m"scientific_uppercase_E"[0m[1;39m: [0m981[0m[1;39m |
| 217 | +[1;39m}[0m |
| 218 | +``` |
| 219 | +
|
| 220 | +HTTPie normalizes numbers differently from jq, and also re-orders field (`scientific_notation_negative` is now before |
| 221 | +`scientific_notation_positive`): |
| 222 | +
|
| 223 | +```shell |
| 224 | +$ http http://localhost:8000/numbers.json |
| 225 | +[34mHTTP[0m/[34m1.0[0m [34m200[0m [36mOK[0m |
| 226 | +[36mContent-Length[0m: 83 |
| 227 | +... |
| 228 | + |
| 229 | +{ |
| 230 | + [1;34m"scientific_notation_negative"[0m: [34m0.00602[0m, |
| 231 | + [1;34m"scientific_notation_positive"[0m: [34m12300.0[0m, |
| 232 | + [1;34m"scientific_uppercase_E"[0m: [34m981.0[0m |
| 233 | +} |
| 234 | +``` |
| 235 | +
|
| 236 | +With Hurl, once again, we've chosen not to normalize anything and just augment user input with colors and spacing: |
| 237 | +
|
| 238 | +```shell |
| 239 | +$ echo 'GET http://localhost:8000/numbers.json' |
| 240 | +[1;39m{[0m |
| 241 | + [1;34m"scientific_notation_positive"[0m[1;39m:[0m [0;36m1.23e4[0m[1;39m,[0m |
| 242 | + [1;34m"scientific_notation_negative"[0m[1;39m:[0m [0;36m6.02e-3[0m[1;39m,[0m |
| 243 | + [1;34m"scientific_uppercase_E"[0m[1;39m:[0m [0;36m9.81E+2[0m |
| 244 | +[1;39m}[0m |
| 245 | +``` |
| 246 | +
|
| 247 | +Which is exactly the JSON response, minus color and spaces: |
| 248 | +
|
| 249 | +``` |
| 250 | +{ |
| 251 | + "scientific_notation_positive": 1.23e4, |
| 252 | + "scientific_notation_negative": 6.02e-3, |
| 253 | + "scientific_uppercase_E": 9.81E+2 |
| 254 | +} |
| 255 | +``` |
| 256 | +
|
| 257 | +
|
| 258 | +If Hurl pretty printing is too minimalist for you, you can still pipe Hurl output through `jq` for instance and it will work. |
| 259 | +When Hurl's output is redirected to a file or through a pipe, all pretty printing is disable, so tools that expect a plain |
| 260 | +JSON response will work as usual. |
| 261 | +
|
| 262 | +## New Predicates isObject, isList |
| 263 | +
|
| 264 | +[Predicates]({% link _docs/asserting-response.md %}#predicates) are used to check HTTP responses: |
| 265 | +
|
| 266 | +```hurl |
| 267 | +GET http://httpbin.org/json |
| 268 | +HTTP 200 |
| 269 | +[Asserts] |
| 270 | +jsonpath "$.slideshow.author" startsWith "Yours Truly" |
| 271 | +jsonpath "$.slideshow.slides[0].title" contains "Wonder" |
| 272 | +jsonpath "$.slideshow.slides" count == 2 |
| 273 | +jsonpath "$.slideshow.date" != null |
| 274 | +``` |
| 275 | +
|
| 276 | +Two new predicates are introduced with 7.1.0: |
| 277 | +
|
| 278 | +- `isObject`: check is a value is an object (when working with JSONPath for instance) |
| 279 | +- `isList`: check if a value is an array |
| 280 | +
|
| 281 | +```hurl |
| 282 | +GET https://example.org/order |
| 283 | +HTTP 200 |
| 284 | +[Asserts] |
| 285 | +jsonpath "$.userInfo" isObject |
| 286 | +jsonpath "$.userInfo.books" isList |
| 287 | +``` |
| 288 | +
|
| 289 | +## New Supported curl options |
| 290 | +
|
| 291 | +Introduced in [Hurl 7.0.0], [`--ntlm`]({% link _docs/manual.md %}#ntlm) and [`--negotiate`]({% link _docs/manual.md %}#negotiate) |
| 292 | +curl options can now be set per request: |
| 293 | +
|
| 294 | +```hurl |
| 295 | +GET http://petfactory.com/sharepoint |
| 296 | +[Options] |
| 297 | +user: alice:1234 |
| 298 | +ntlm: true |
| 299 | +HTTP 200 |
| 300 | +``` |
| 301 | +
|
| 302 | +## New Filters utf8Decode, utf8Encode |
| 303 | +
|
| 304 | +[Filters]({% link _docs/filters.md %}) allow to transform data extracted from HTTP responses. In the following sample, `replaceRegex`, `split`, |
| 305 | +`count` and `nth` are filters that process input; they can be chained to transform values in asserts and captures: |
| 306 | +
|
| 307 | +```hurl |
| 308 | +GET https://example.org/api |
| 309 | +HTTP 200 |
| 310 | +[Captures] |
| 311 | +name: jsonpath "$.user.id" replaceRegex /\d/ "x" |
| 312 | +[Asserts] |
| 313 | +header "x-servers" split "," count == 2 |
| 314 | +header "x-servers" split "," nth 0 == "rec1" |
| 315 | +header "x-servers" split "," nth 1 == "rec3" |
| 316 | +jsonpath "$.books" count == 12 |
| 317 | +``` |
| 318 | +
|
| 319 | +In Hurl 7.1.0, we've added new filters `utf8Decode` and `utf8Encode` to encode and decode from bytes to string. In the next |
| 320 | +example, we get bytes from a Base64 encoded string, then decode these bytes to a string using UTF-8 encoding: |
| 321 | +
|
| 322 | +```hurl |
| 323 | +GET https://example.org/messages |
| 324 | +HTTP 200 |
| 325 | +[Asserts] |
| 326 | +# From a Base64 string to UTF-8 bytes to final string |
| 327 | +jsonpath "$.bytesInBase64" base64Decode utf8Decode == "Hello World" |
| 328 | +``` |
| 329 | +
|
| 330 | +## __That's all for today!__ |
| 331 | +
|
| 332 | +There are a lot of other improvements with Hurl 7.1.0 and also a lot of bug fixes. Among other things, we have added |
| 333 | +the following features to 7.1.0: |
| 334 | +
|
| 335 | +- new ways to add [secrets]({% link _docs/templates.md %}#secrets) |
| 336 | + - by setting environment variables `HURL_SECRET_my_secret` |
| 337 | + - using secrets files with [`--secrets-file`]({% link _docs/manual.md %}#secrets-file) |
| 338 | +- improve `--test` progress bar to display retry status |
| 339 | +- small improvments to HTML report |
| 340 | +
|
| 341 | +You can check the complete list of enhancements and bug fixes [in our release note]. |
| 342 | +
|
| 343 | +If you like Hurl, don't hesitate to [support us with a star on GitHub] and share it on [𝕏] and [Bluesky]! |
| 344 | +
|
| 345 | +We'll be happy to hear from you, either for enhancement requests or for sharing your success story using Hurl! |
| 346 | +
|
| 347 | +
|
| 348 | +[Hurl]: https://hurl.dev |
| 349 | +[Hurl 7.1.0]: https://github.com/Orange-OpenSource/hurl/releases/tag/7.1.0 |
| 350 | +[curl]: https://curl.se |
| 351 | +[in our release note]: https://github.com/Orange-OpenSource/hurl/releases/tag/7.1.0 |
| 352 | +[Bluesky]: https://bsky.app/profile/hurldev.bsky.social |
| 353 | +[support us with a star on GitHub]: https://github.com/Orange-OpenSource/hurl/stargazers |
| 354 | +[OAuth]: https://en.wikipedia.org/wiki/OAuth |
| 355 | +[CSRF]: https://en.wikipedia.org/wiki/Cross-site_request_forgery |
| 356 | +[Hurl 7.0.0]: https://hurl.dev/blog/2025/07/30/announcing-hurl-7.0.0.html |
| 357 | +[𝕏]: https://x.com/HurlDev |
| 358 | +
|
0 commit comments