21 exercises in cURL

HTTP magic at your terminal fingertips

The 21 curl exercises by Julia Evans are a compact collection of assignments to practice the usage of cURL.

Here is my take at it:

  1. Request https://httpbin.org:

    › curl -X GET 'https://httpbin.org'
    
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
     <meta charset="UTF-8">
    [...]

  2. Request https://httpbin.org/anything. httpbin.org/anything will look at the request you made, parse it, and echo back to you what you requested. curl’s default is to make a GET request:

    › curl -X GET 'https://httpbin.org/anything'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "GET",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  • As curl’s default is GET the -X GET could be omitted.
  1. Make a POST request to https://httpbin.org/anything

    › curl -X POST 'https://httpbin.org/anything'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Content-Length": "0",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "POST",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  2. Make a GET request to https://httpbin.org/anything, but this time add some query parameters (set value=panda):

    › curl -X GET 'https://httpbin.org/anything' -d 'value=pada'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {
    "value": "pada"
    },
    "headers": {
    "Accept": "_/_",
    "Content-Length": "10",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "GET",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  3. Request google’s robots.txt file (www.google.com/robots.txt):

    › curl -X GET 'https://www.google.com/robots.txt'
    
    User-agent: \*
    Disallow: /search
    Allow: /search/about
    Allow: /search/static
    [...]

  4. Make a GET request to https://httpbin.org/anything and set the header User-Agent: elephant.

    › curl -X GET 'https://httpbin.org/anything' -H 'User-Agent: elephant'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Host": "httpbin.org",
    "User-Agent": "elephant"
    },
    "json": null,
    "method": "GET",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  5. Make a DELETE request to https://httpbin.org/anything

    › curl -X DELETE 'https://httpbin.org/anything'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "DELETE",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  6. Request https://httpbin.org/anything and also get the response headers

    › curl -X GET 'https://httpbin.org/anything' -i
    
    HTTP/1.1 200 OK
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Origin: \*
    Content-Type: application/json
    [...]
    {
    "args": {},
    [...]
    }

  7. Make a POST request to https://httpbin.com/anything with the JSON body {“value”: “panda”}:

    › curl -X POST 'https://httpbin.org/anything' -d '{"value": "panda"}'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {
    "{\"value\": \"panda\"}": ""
    },
    "headers": {
    "Accept": "_/_",
    "Content-Length": "18",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "POST",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  • The transmitted data ends up in the "form" field.
  1. Make the same POST request as the previous exercise, but set the Content-Type header to application/json (because POST requests need to have a content type that matches their body). Look at the json field in the response to see the difference from the previous one:
    › curl -X POST 'https://httpbin.org/anything' \
     -H 'Content-Type: application/json' \
     -d '{"value": "panda"}'
    
    {
    "args": {},
    "data": "{\"value\": \"panda\"}",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Content-Length": "18",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": {
    "value": "panda"
    },
    "method": "POST",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }
  • Nice, the data got explicitly recognised as JSON data.
  1. Make a GET request to https://httpbin.org/anything and set the header Accept-Encoding: gzip (what happens? why?):
    › curl -X GET 'https://httpbin.org/anything' -H 'Accept-Encoding: gzip'  
    M?A? E????4
    ??it?´h@?F?.Z??
    ??ߟ??s$??yI???X?$??lE?B?\*=?c?2??tF?4l?~A??m?ᦵl?{̿b<?]?u??v| ???{?ӊ3?&?I???.9:A??o???r???????????????`??k??Y??,?ڣ?ȿ?)1ja"????DK?K?
    @%  
  • Looks like whatever got transferred, was ‘gzip’ compressed and got spat out into the shell.
  1. Put a bunch of a JSON in a file and then make a POST request to https://httpbin.org/anything with the JSON in that file as the body:

    › curl -X POST 'https://httpbin.org/anything' \
     -H 'Content-Type: application/json' \
     -d @bunchof.json
    
    {
    "args": {},
    "data": "{ \"firstName\": \"John\", \"lastName\": \"Smith\", \"isAlive\": true, \"age\": 27, \"address\": { \"streetAddress\": \"21 2nd Street\", \"city\": \"New York\", \"state\": \"NY\", \"postalCode\": \"10021-3100\" }, \"phoneNumbers\": [ { \"type\": \"home\", \"number\": \"212 555-1234\" }, { \"type\": \"office\", \"number\": \"646 555-4567\" }, { \"type\": \"mobile\", \"number\": \"123 456-7890\" } ], \"children\": [], \"spouse\": null}",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Content-Length": "447",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": {
    "address": {
    "city": "New York",
    "postalCode": "10021-3100",
    "state": "NY",
    "streetAddress": "21 2nd Street"
    },
    "age": 27
    [...]
    },
    "method": "POST",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  2. Make a request to https://httpbin.org/image and set the header ‘Accept: image/png’. Save the output to a PNG file and open the file in an image viewer. Try the same thing with with different Accept: headers:

    › curl -X GET 'https://httpbin.org/image' -H 'Accept: image/png' -o someImage.png
    
    % Total % Received % Xferd Average Speed Time Time Time Current
    Dload Upload Total Spent Left Speed
    100 8090 100 8090 0 0 18879 0 --:--:-- --:--:-- --:--:-- 18901

  • Nice a piglet
  • Also other image formats are available. Try yourself with curl -X GET 'https://httpbin.org/image'
  1. Make a PUT request to https://httpbin.org/anything :

    › curl -X PUT 'https://httpbin.org/anything'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Content-Length": "0",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "PUT",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }

  2. Request https://httpbin.org/image/jpeg, save it to a file, and open that file in your image editor:

    › curl -X GET 'https://httpbin.org/image/jpeg' -o someJpeg.jpeg
    
    % Total % Received % Xferd Average Speed Time Time Time Current
    Dload Upload Total Spent Left Speed
    100 35588 100 35588 0 0 63869 0 --:--:-- --:--:-- --:--:-- 63777

  • Is that a Black-backed jackal? Black-backed jackal
  1. Request https://www.twitter.com. You’ll get an empty response. Get curl to show you the response headers too, and try to figure out why the response was empty:
    › curl -X GET 'https://www.twitter.com' -i
    
    HTTP/2 301
    content-length: 0
    date: Fri, 30 Aug 2019 16:23:25 GMT
    location: https://twitter.com/
    server: tsa_o
    set-cookie: personalization_id="v1_Q9zuTfFFOE9k7hSxDhPo8g=="; Max-Age=63072000; Expires=Sun, 29 Aug 2021 16:23:25 GMT; Path=/; Domain=.twitter.com
    set-cookie: guest_id=v1%3A156718220573814829; Max-Age=63072000; Expires=Sun, 29 Aug 2021 16:23:25 GMT; Path=/; Domain=.twitter.com
    strict-transport-security: max-age=631138519
    x-connection-hash: d98cf4f8396275a8ba48bedc5451fbc6
    x-response-time: 118
  • With content-length: 0 the response is indeed empty. The status message HTTP/2 301 signals that the content was Moved Permanently to location: https://twitter.com/.
  1. Make any request to https://httpbin.org/anything and just set some nonsense headers (like panda: elephant):
    › curl -X GET 'https://httpbin.org/anything' -H 'panda: elephant'
    
    {
    "args": {},
    "data": "",
    "files": {},
    "form": {},
    "headers": {
    "Accept": "_/_",
    "Host": "httpbin.org",
    "Panda": "elephant",
    "User-Agent": "curl/7.54.0"
    },
    "json": null,
    "method": "GET",
    "origin": "94.79.129.22, 94.79.129.22",
    "url": "https://httpbin.org/anything"
    }
  • No mayor changes besides that the header gets transferred unaltered, which makes it a nice vector for control- and meta-data.
  1. Request https://httpbin.org/status/404 and https://httpbin.org/status/200. Request them again and get curl to show the response headers:
    › curl -X GET 'https://httpbin.org/status/404' -i
    
    HTTP/1.1 404 NOT FOUND
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Origin: \*
    [...]
> › curl -X GET 'https://httpbin.org/status/200' -i

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: \*
[...]
  • The responses are almost the same, exept that the HTTP status messages differ, HTTP/1.1 404 NOT FOUND vs. HTTP/1.1 200 OK.
  1. Request https://httpbin.org/anything and set a username and password (with -u username:password)
    › curl -X GET 'https://httpbin.org/anything' -u 'username:password' -v
    [...]
    
    - Server auth using Basic with user 'username'
      > GET /anything HTTP/1.1
      > Host: httpbin.org
      > Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
      > [...]
      > {
      > "args": {},
      > "data": "",
      > "files": {},
      > "form": {},
      > "headers": {
       "Accept": "*/*",
       "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
       "Host": "httpbin.org",
       "User-Agent": "curl/7.54.0"
      },
      "json": null,
      "method": "GET",
      "origin": "94.79.129.22, 94.79.129.22",
      "url": "https://httpbin.org/anything"
      }
      [...]
      
  • Most important thing here is see what did cURL do with the -u 'username:password' option, therefor the -v. It transformed it into a Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= header field. Where the “username:password” got Base64 encoded.
  • And be warned this about as secret as the plaintext of it!
  1. Download the Twitter homepage (https://twitter.com) in Spanish by setting the Accept-Language: es-ES header.
    › curl -X GET 'https://twitter.com' -H 'Accept-Language: es-Es'
    
    [...]
  • Could be Spanish…, to much content to parse with my eyes.
  1. Make a request to the Stripe API with curl. (see https://stripe.com/docs/development for how, they give you a test API key). Try making exactly the same request to https://httpbin.org/anything:
    #!/bin/sh
    curl https://api.stripe.com/v1/payment_intents \
     -u sk_test_Oy**\*\*\***: \
     -d amount=999 \
     -d currency=eur \
     -d payment_method_types[]=card \
     -d receipt_email="jenny.rosen@example.com"
    
    {
    "id": "pi_1FD**\*\*\***",
    "object": "payment_intent",
    "amount": 999,
    "amount_capturable": 0,
    "amount_received": 0,
    "application": null,
    "application_fee_amount": null,
    "canceled_at": null,
    "cancellation_reason": null,
    [...]
  • To get the API key you need to do the little sign up dance, but with the examples on their site, this went smoothly.
  • Not so sure about the second part of the exercise, as well as I don’t publish the test API key here, I would not send that API key to any foreign server. So I didn’t.

All in all a nice exercise. Julia Evans, thank you for the inspiration.