HTTP messages
Sending a client request - MDN
Once the connection is established, the user-agent can send the request (a user-agent is typically a web browser, but can be anything else, a crawler, for example). A client request consists of text directives, separated by CRLF (carriage return, followed by line feed), divided into three blocks:
- The first line contains a request method followed by its parameters:
- the path of the document, as an absolute URL without the protocol or domain name
- the HTTP protocol version
- Subsequent lines represent an HTTP header, giving the server information about what type of data is appropriate (for example, what language, what MIME types), or other data altering its behavior (for example, not sending an answer if it is already cached). These HTTP headers form a block which ends with an empty line.
- The final block is an optional data block, which may contain further data mainly used by the POST method.
Structure of a server response
After the connected agent has sent its request, the web server processes it, and ultimately returns a response. Similar to a client request, a server response is formed of text directives, separated by CRLF, though divided into three blocks:
- The first line, the status line, consists of an acknowledgment of the HTTP version used, followed by a response status code (and its brief meaning in human-readable text).
- Subsequent lines represent specific HTTP headers, giving the client information about the data sent (for example, type, data size, compression algorithm used, hints about caching). Similarly to the block of HTTP headers for a client request, these HTTP headers form a block ending with an empty line.
- The final block is a data block, which contains the optional data.
HTTP messages are the mechanism used to exchange data between a server and a client in the HTTP protocol. There are two types of messages: requests sent by the client to trigger an action on the server, and responses, the answer that the server sends in response to a request.
Both requests and responses share a similar structure:
- A start-line is a single line that describes the HTTP version along with the request method or the outcome of the request.
- An optional set of HTTP headers containing metadata that describes the message. For example, a request for a resource might include the allowed formats of that resource, while the response might include headers to indicate the actual format returned.
- An empty line indicating the metadata of the message is complete.
- An optional body containing data associated with the message. This might be POST data to send to the server in a request, or some resource returned to the client in a response. Whether a message contains a body or not is determined by the start-line and HTTP headers.
How to make a webserver with netcat (nc)
ubuntu@LAPTOP-JBell:~$ printf "GET / HTTP/1.1\r\nHost: developer.mozilla.org\r\n\r\n" | nc developer.mozilla.org 80
HTTP/1.1 301 Moved Permanently
Cache-Control: private
Location: https://developer.mozilla.org:443/
Content-Length: 0
Date: Sun, 05 Oct 2025 08:01:48 GMT
Content-Type: text/html; charset=UTF-8
ubuntu@LAPTOP-JBell:~$ netcat developer.mozilla.org 80
GET /en-US/docs/Web/HTTP/Guides/Messages HTTP/1.1
HTTP/1.1 301 Moved Permanently
Cache-Control: private
Location: https://developer.mozilla.org:443/en-US/docs/Web/HTTP/Guides/Messages
Content-Length: 0
Date: Sun, 05 Oct 2025 14:47:29 GMT
Content-Type: text/html; charset=UTF-8
ubuntu@LAPTOP-JBell:~$ printf "GET / HTTP/1.1\r\nHost: httpbin.org\r\n\r\n" | nc httpbin.org 80 | head -n 10
HTTP/1.1 200 OK
Date: Sun, 05 Oct 2025 16:20:18 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 9593
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
<!DOCTYPE html>
Flask localhost example
Python and REST APIs: Interacting With Web Services - Real Python
We run a local Flask webserver.
(real_python) ubuntu@LAPTOP-JBell:~/real_python$ flask run
* Serving Flask app 'app.py'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
127.0.0.1 - - [05/Oct/2025 13:42:08] "GET / HTTP/1.1" 404 -
GET
Running netcat with the -C
option (send CRLF as line-ending),
nc -C localhost 5000
GET /countries HTTP/1.1
Host: localhost
Content-Type: application/json; charset=UTF-8
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.13.7
Date: Sun, 05 Oct 2025 17:59:16 GMT
Content-Type: application/json
Content-Length: 184
Connection: close
[{"area":513120,"capital":"Bangkok","id":1,"name":"Thailand"},{"area":7617930,"capital":"Canberra","id":2,"name":"Australia"},{"area":1010408,"capital":"Cairo","id":3,"name":"Egypt"}]
POST
We can make a POST request three ways: (we could also do this for GET request above)
curl
First, using curl.
curl -i http://127.0.0.1:5000/countries \
-X POST \
-H 'Content-Type: application/json' \
-d '{"name":"GermanyA", "capital": "Berlin", "area": 357022}'
{"area":357022,"capital":"Berlin","id":4,"name":"GermanyA"}
netcat
Second, using netcat.
First we manually find Content-Length.
ubuntu@LAPTOP-JBell:~$ printf '%s' '{"name": "GermanyB", "capital": "Berlin", "area": 357022}' | wc -c
57
Now we run netcat with -C
to use CRLF line endings.
ubuntu@LAPTOP-JBell:~$ nc -C localhost 5000
POST /countries HTTP/1.1
Host: localhost
Content-Type: application/json
Content-Length: 57
{"name": "GermanyB", "capital": "Berlin", "area": 357022}
HTTP/1.1 201 CREATED
Server: Werkzeug/3.1.3 Python/3.13.7
Date: Sun, 05 Oct 2025 19:40:30 GMT
Content-Type: application/json
Content-Length: 60
Connection: close
{"area":357022,"capital":"Berlin","id":5,"name":"GermanyB"}
urllib.request
Third, using urllib.request
.
from urllib.request import urlopen, Request
import json
post_dict = {"capital": "Berlin", "name": "GermanyC", "area": 357022}
post_json_payload = json.dumps(post_dict)
post_data = post_json_payload.encode("utf-8")
post_headers = {"Content-Type": "application/json"}
url = "http://localhost:5000/countries"
request = Request(url, headers=post_headers, data=post_data)
with urlopen(request, timeout=10) as response:
content_bytes = response.read()
content_dict_list = json.loads(content_bytes)
# {'area': 357022, 'capital': 'Berlin', 'id': 6, 'name': 'GermanyC'}
requests
Fourth, using requests
.
response = requests.post("http://127.0.0.1:5000/countries", json={"name": "GermanyD", "capital": "Berlin", "area": 357022})
response.json()
# {'area': 357022, 'capital': 'Berlin', 'id': 7, 'name': 'GermanyD'}