HTTP vs HTTPS Explained — How the Web Actually Talks to You
Every time you open a browser and type a web address, your computer and a server thousands of miles away have a conversation. That conversation has rules — a protocol — and understanding those rules is one of the most practically useful things you can learn as a developer. HTTP and HTTPS aren't just acronyms you nod at; they directly affect whether your users' passwords get stolen, whether Google ranks your site, and whether your API calls succeed or silently fail.
The problem HTTP was designed to solve is simple: two computers need a shared language to ask for and deliver web content. But HTTP was invented in 1991, before the internet was a place where you'd type your credit card number. It was designed for speed and simplicity, not privacy. HTTPS was the answer to that gap — it wraps HTTP in a security layer so that the conversation between browser and server is private, verified, and tamper-proof.
By the end of this article you'll be able to explain what happens step-by-step when a browser fetches a web page, articulate exactly why HTTPS matters and what it actually protects (and what it doesn't), read and understand real HTTP request and response headers, and make correct HTTP and HTTPS decisions in your own projects. No hand-waving — actual understanding.
What HTTP Is and How a Browser Actually Fetches a Page
HTTP stands for HyperText Transfer Protocol. 'Protocol' just means a set of agreed rules — like how a phone call always starts with 'hello' before you say anything else. HTTP is the rulebook that browsers and web servers follow when talking to each other.
Here's what happens when you type http://example.com and press Enter:
- Your browser looks up the IP address for
example.comusing DNS (think of DNS as the internet's phone book). - Your browser opens a TCP connection to that IP address on port 80 (HTTP's default port).
- Your browser sends an HTTP request — a structured text message saying 'please give me the homepage'.
- The server reads that request, finds the resource, and sends back an HTTP response — another structured text message containing the actual HTML.
- Your browser reads the HTML and paints the page.
Every single image, stylesheet, and script on a page is a separate HTTP request going through this same loop. A modern webpage can fire off 50–100 requests just to load fully.
The critical thing to understand: all of this text — including any form data you submit — travels as plain, readable text across the network. Anyone sitting on the same Wi-Fi as you can intercept and read every byte.
import socket # We're going to manually craft an HTTP/1.1 request using just a raw TCP socket. # This is exactly what your browser does under the hood, just automated. HOST = "example.com" # The server we want to talk to PORT = 80 # Port 80 is the standard port for plain HTTP # Step 1: Create a TCP socket — this is our 'phone line' to the server client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Step 2: Connect to the server on port 80 client_socket.connect((HOST, PORT)) # Step 3: Build the HTTP request as a plain text string. # Every HTTP request has: a request line, headers, and a blank line at the end. http_request = ( "GET / HTTP/1.1\r\n" # GET the root path (/), using HTTP version 1.1 "Host: example.com\r\n" # The Host header tells the server which site we want "Connection: close\r\n" # Tell the server to close the connection after responding "\r\n" # Blank line signals the end of the headers ) # Step 4: Send the request — notice it's just text, nothing encrypted client_socket.sendall(http_request.encode("utf-8")) # Step 5: Receive the response in chunks response_parts = [] while True: chunk = client_socket.recv(4096) # Receive up to 4096 bytes at a time if not chunk: break # Empty chunk means the server closed the connection response_parts.append(chunk) client_socket.close() # Decode the full response from bytes back to a readable string full_response = b"".join(response_parts).decode("utf-8", errors="replace") # Split the response into headers and body — they're separated by a blank line header_section, _, body_section = full_response.partition("\r\n\r\n") print("=== HTTP RESPONSE HEADERS ===") print(header_section) print("\n=== FIRST 300 CHARS OF BODY ===") print(body_section[:300])
HTTP/1.1 200 OK
Content-Encoding: gzip
Accept-Ranges: bytes
Age: 307430
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Mon, 01 Jan 2025 12:00:00 GMT
Etag: "3147526947"
Expires: Mon, 08 Jan 2025 12:00:00 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (dce/26C7)
X-Cache: HIT
Content-Length: 648
Connection: close
=== FIRST 300 CHARS OF BODY ===
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Why HTTP Alone Is Dangerous — The Man in the Middle
Here's a scenario that actually happens. You're at a coffee shop, connected to the free Wi-Fi. You log into a website over plain HTTP. The login form sends your username and password to the server as an HTTP POST request. That request is just text, and it travels through the coffee shop's router before it ever reaches the internet.
The person running that router — or anyone with a packet-sniffing tool like Wireshark — can read that request verbatim. Your username: visible. Your password: visible. Your session cookie (which is as good as your password): visible. This attack is called a Man-in-the-Middle (MITM) attack because the attacker sits between you and the server, reading everything.
HTTP has three specific weaknesses:
1. No Privacy. Everything is plaintext. Anyone on the network path can read it.
2. No Integrity. An attacker in the middle can modify the data before it reaches you. They could swap out a bank's account number in a response and you'd never know.
3. No Authentication. When you connect to http://mybank.com, HTTP gives you no proof that you're actually talking to your bank and not an attacker's fake server.
HTTPS solves all three of these problems simultaneously using a system called TLS.
# This script does NOT actually sniff packets (that requires root and special libraries). # Instead it illustrates what an attacker SEES on the wire with plain HTTP # versus what they see with HTTPS. This is conceptual but 100% accurate. # === WHAT AN ATTACKER SEES WITH PLAIN HTTP === # Imagine this is the raw bytes flowing over the network during an HTTP login http_login_request_visible_to_attacker = """ POST /login HTTP/1.1 Host: mycoolbank.com Content-Type: application/x-www-form-urlencoded Content-Length: 39 username=alice&password=SuperSecret123 """ # Every character above is readable by anyone on the network. # The attacker now has Alice's password. print("=== ATTACKER'S VIEW: Plain HTTP ===") print("Intercepted request (fully readable):") print(http_login_request_visible_to_attacker) print("-" * 50) # === WHAT AN ATTACKER SEES WITH HTTPS (TLS encrypted) === # After the TLS handshake, all data is encrypted with a session key # that only the client and server know. The attacker sees gibberish. # This simulates the encrypted bytes — real TLS ciphertext looks like random noise. import os random_encrypted_bytes = os.urandom(64) # 64 random bytes, like real TLS ciphertext print("=== ATTACKER'S VIEW: HTTPS (TLS encrypted) ===") print("Intercepted bytes (encrypted, unreadable):") print(random_encrypted_bytes) print("\nAll the attacker knows: the destination IP and the approximate size of the request.") print("They cannot read the username, password, or any content.")
Intercepted request (fully readable):
POST /login HTTP/1.1
Host: mycoolbank.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 39
username=alice&password=SuperSecret123
--------------------------------------------------
=== ATTACKER'S VIEW: HTTPS (TLS encrypted) ===
Intercepted bytes (encrypted, unreadable):
b'\x8f\x3a\x11\xc4\x7e\x02\xbb\xd9\x45\xf1\x23\x9c...'
All the attacker knows: the destination IP and the approximate size of the request.
They cannot read the username, password, or any content.
How HTTPS Works — TLS, Certificates, and the Handshake Explained
HTTPS is just HTTP with TLS layered underneath it. TLS (Transport Layer Security) is the security protocol that provides the three things HTTP lacks: privacy, integrity, and authentication. You'll sometimes see the older name SSL — SSL was the original protocol, TLS replaced it, but people still say 'SSL certificate' out of habit. They mean TLS.
Here's how HTTPS solves each problem:
Authentication via Certificates: When your browser connects to https://mybank.com, the server sends a digital certificate — like a government-issued ID card. This certificate says 'I am mybank.com and a trusted authority called a Certificate Authority (CA) has verified this'. Your browser ships with a built-in list of trusted CAs (like Let's Encrypt, DigiCert, Comodo). If the certificate is signed by one of them and matches the domain, the browser trusts it.
The TLS Handshake: Before any HTTP data is exchanged, the browser and server do a 'handshake' to agree on how they'll encrypt the session. They negotiate an encryption algorithm and securely exchange a session key using asymmetric cryptography (public/private key pairs). After the handshake, all data is encrypted with a fast symmetric cipher using that session key.
Privacy and Integrity: Once the session key is established, all HTTP traffic is encrypted and signed. An attacker can't read it (privacy) and can't modify it without the modification being detected (integrity).
HTTPS uses port 443 by default, not port 80.
import ssl import socket import json # Let's make an HTTPS request manually — similar to what we did with HTTP, # but this time we wrap our socket in TLS using Python's ssl module. HOST = "httpbin.org" # A public API that reflects back request info — great for testing PORT = 443 # Port 443 is the standard port for HTTPS # Step 1: Create a raw TCP socket (same as before) raw_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Step 2: Create an SSL context — this is the TLS configuration. # PROTOCOL_TLS_CLIENT automatically uses the most secure TLS version available. # check_hostname=True and verify_mode=CERT_REQUIRED mean: # - The server MUST present a valid certificate # - The certificate's domain MUST match HOST # This is how HTTPS provides authentication. tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) tls_context.check_hostname = True # Verify the domain name matches tls_context.verify_mode = ssl.CERT_REQUIRED # Reject invalid/untrusted certs # Step 3: Wrap our plain socket in TLS — this triggers the TLS handshake. # server_hostname is used for SNI (Server Name Indication) — it lets one server # host multiple HTTPS sites on the same IP. secure_socket = tls_context.wrap_socket(raw_socket, server_hostname=HOST) # Step 4: Connect — the TLS handshake happens automatically during wrap_socket secure_socket.connect((HOST, PORT)) # Step 5: Inspect the certificate the server sent us server_certificate = secure_socket.getpeercert() print("=== SERVER CERTIFICATE INFO ===") print(f"Issued to: {server_certificate.get('subject')}") print(f"Issued by: {server_certificate.get('issuer')}") print(f"Valid from: {server_certificate.get('notBefore')}") print(f"Valid until:{server_certificate.get('notAfter')}") print(f"TLS version in use: {secure_socket.version()}") # Step 6: Now send an HTTP request — same format as before, but encrypted by TLS http_request = ( "GET /get HTTP/1.1\r\n" # /get endpoint returns request info as JSON f"Host: {HOST}\r\n" "Connection: close\r\n" "\r\n" ) secure_socket.sendall(http_request.encode("utf-8")) # Step 7: Receive the encrypted response (TLS decrypts it transparently for us) response_chunks = [] while True: chunk = secure_socket.recv(4096) if not chunk: break response_chunks.append(chunk) secure_socket.close() full_response = b"".join(response_chunks).decode("utf-8", errors="replace") # The response has headers and a body — split on the blank line header_section, _, body_section = full_response.partition("\r\n\r\n") print("\n=== HTTP RESPONSE STATUS ===") print(header_section.split("\r\n")[0]) # Just the first line, e.g., 'HTTP/1.1 200 OK' # Parse the JSON body to show the request info the server echoed back try: response_data = json.loads(body_section) print("\n=== SERVER SAW OUR REQUEST ORIGIN ===") print(f"Our IP address (as seen by server): {response_data.get('origin')}") print(f"URL requested: {response_data.get('url')}") except json.JSONDecodeError: print("(Could not parse JSON — check your network connection)")
Issued to: ((('commonName', 'httpbin.org'),),)
Issued by: ((('commonName', "Let's Encrypt"),),)
Valid from: Jan 1 00:00:00 2025 GMT
Valid until:Apr 1 00:00:00 2025 GMT
TLS version in use: TLSv1.3
=== HTTP RESPONSE STATUS ===
HTTP/1.1 200 OK
=== SERVER SAW OUR REQUEST ORIGIN ===
Our IP address (as seen by server): 203.0.113.42
URL requested: https://httpbin.org/get
HTTP Status Codes, Request Methods, and Headers You'll Use Daily
Understanding the structure of HTTP messages will save you hours of debugging API calls and network issues. Every HTTP conversation has a request from the client and a response from the server, and both follow a strict format.
HTTP Methods tell the server what action you want: - GET — Retrieve a resource. Should never change data. - POST — Send data to create a resource. - PUT — Replace an existing resource entirely. - PATCH — Update part of an existing resource. - DELETE — Remove a resource.
Status Codes are the server's short reply on how things went: - 200 OK — All good. - 201 Created — Resource was created (usually after POST). - 301 Moved Permanently — The URL has changed forever. - 400 Bad Request — Your request was malformed. - 401 Unauthorized — You need to log in. - 403 Forbidden — Logged in but not allowed. - 404 Not Found — Resource doesn't exist. - 500 Internal Server Error — The server broke.
Headers are key-value metadata pairs on both requests and responses. Content-Type tells the receiver what format the body is in. Authorization carries authentication tokens. Cache-Control governs caching behaviour.
import urllib.request import urllib.error import json # We'll use httpbin.org — a free service that mirrors back whatever you send it. # This is the same site professional developers use to test HTTP behaviour. BASE_URL = "https://httpbin.org" def make_request(method: str, path: str, payload: dict = None) -> None: """Send an HTTP request and print the status code and response summary.""" url = f"{BASE_URL}{path}" # Encode the payload as JSON bytes if there is one body_bytes = json.dumps(payload).encode("utf-8") if payload else None # Build the request object with the correct method and headers request = urllib.request.Request( url, data=body_bytes, method=method, headers={ "Content-Type": "application/json", # Tell server we're sending JSON "Accept": "application/json", # Tell server we want JSON back "User-Agent": "TheCodeForge-Demo/1.0" # Identify our client } ) try: with urllib.request.urlopen(request) as response: status_code = response.status # e.g., 200, 201 response_body = json.loads(response.read().decode("utf-8")) print(f"[{method}] {url}") print(f" Status: {status_code}") # For POST/PUT, the server echoes back the JSON data we sent if "json" in response_body: print(f" Server received our JSON: {response_body['json']}") else: # For GET, show the request headers the server saw print(f" Headers server saw: User-Agent = {response_body.get('headers', {}).get('User-Agent')}") except urllib.error.HTTPError as error: # HTTPError is raised for 4xx and 5xx status codes print(f"[{method}] {url}") print(f" Status: {error.code} — {error.reason}") print() # --- Demonstrate different HTTP methods --- # GET: Retrieve data — no body, just headers make_request("GET", "/get") # POST: Send a new user object to 'create' it new_user = {"name": "Alice", "email": "alice@example.com", "role": "developer"} make_request("POST", "/post", payload=new_user) # PUT: Replace a user record entirely updated_user = {"name": "Alice Smith", "email": "alice.smith@example.com", "role": "senior-developer"} make_request("PUT", "/put", payload=updated_user) # DELETE: Remove a resource — httpbin echoes this back too make_request("DELETE", "/delete") # 404 example — request a path that doesn't exist make_request("GET", "/this-page-does-not-exist")
Status: 200
Headers server saw: User-Agent = TheCodeForge-Demo/1.0
[POST] https://httpbin.org/post
Status: 200
Server received our JSON: {'name': 'Alice', 'email': 'alice@example.com', 'role': 'developer'}
[PUT] https://httpbin.org/put
Status: 200
Server received our JSON: {'name': 'Alice Smith', 'email': 'alice.smith@example.com', 'role': 'senior-developer'}
[DELETE] https://httpbin.org/delete
Status: 200
Headers server saw: User-Agent = TheCodeForge-Demo/1.0
[GET] https://httpbin.org/this-page-does-not-exist
Status: 404 — NOT FOUND
| Feature / Aspect | HTTP | HTTPS |
|---|---|---|
| Full name | HyperText Transfer Protocol | HTTP Secure (HTTP + TLS) |
| Default port | 80 | 443 |
| Data in transit | Plain text — readable by anyone on the network | Encrypted — unreadable without the session key |
| Server authentication | None — you can't verify who you're talking to | Yes — via TLS certificate signed by a CA |
| Data integrity | None — data can be modified in transit silently | Guaranteed — tampering is detected and rejected |
| Browser indicator | No padlock; 'Not Secure' warning in Chrome/Firefox | Padlock icon; URL shown as https:// |
| SEO impact | Google ranks HTTP sites lower | Google uses HTTPS as a ranking signal |
| Performance | Slightly faster (no handshake overhead) | Modern TLS 1.3 is nearly as fast; HTTP/2 requires HTTPS |
| Certificate cost | N/A | Free via Let's Encrypt; paid options for extended validation |
| Use case today | Internal development/localhost only | Everything on the public internet — no exceptions |
🎯 Key Takeaways
- HTTP is plain text — every byte of an HTTP conversation is readable by anyone on the network path between client and server. Never use it for anything sensitive on the public internet.
- HTTPS = HTTP + TLS. TLS provides three guarantees simultaneously: encryption (privacy), certificates (authentication that you're talking to the right server), and message signing (integrity, so data can't be silently modified in transit).
- The TLS handshake uses asymmetric cryptography (public/private keys) to securely agree on a shared session key, then switches to fast symmetric encryption for all actual data. This is why HTTPS is barely slower than HTTP in practice.
- 401 vs 403, the meaning of status code families (2xx = success, 3xx = redirect, 4xx = client error, 5xx = server error), and the difference between GET/POST/PUT/PATCH/DELETE are not just trivia — you'll use them every single day as a developer.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using HTTP for localhost development and assuming production HTTPS will 'just work the same' — Symptom: Cookies with the
Secureflag don't get sent locally, mixed-content errors appear in production, or CORS behaviour differs — Fix: Use a self-signed certificate for local development with a tool likemkcertso your local environment mirrors production exactly. - ✕Mistake 2: Thinking HTTPS means the website is safe or trustworthy — Symptom: Users see a padlock and assume they can't be phished or scammed — Fix: Understand and communicate that HTTPS only means the connection is encrypted. A phishing site can have a perfectly valid HTTPS certificate. HTTPS authenticates the domain, not the legitimacy of the business behind it.
- ✕Mistake 3: Forgetting to redirect HTTP to HTTPS and leaving both active — Symptom: Your site works on both
http://yoursite.comandhttps://yoursite.com, meaning users who type the domain withouthttps://are exposed to the insecure version — Fix: Set up a permanent 301 redirect from all HTTP traffic to HTTPS at the server or load balancer level, and add an HTTP Strict Transport Security (HSTS) header to force browsers to always use HTTPS.
Interview Questions on This Topic
- QCan you walk me through exactly what happens when a user types 'https://google.com' in their browser and presses Enter — from DNS lookup through to the page rendering?
- QWhat's the difference between symmetric and asymmetric encryption, and which one does TLS use for the actual data transfer — and why?
- QIf a site is served over HTTPS, does that mean it's secure? What are the security guarantees HTTPS actually provides, and what are the things it does NOT protect against?
Frequently Asked Questions
Does HTTPS make my website completely secure?
No. HTTPS secures the data while it travels between the browser and your server — that's it. It doesn't protect against server-side vulnerabilities like SQL injection, cross-site scripting (XSS), or a poorly secured database. Think of HTTPS as an armoured truck transporting your data: it protects the journey, but not what happens at the destination.
Is HTTP ever acceptable to use in 2024?
For localhost development only. Any website, API, or service accessible on the public internet should use HTTPS without exception. Browsers actively warn users about HTTP sites, Google penalises them in search rankings, and modern browser features like geolocation, service workers, and camera access require HTTPS. There's also a free certificate option via Let's Encrypt, so there's no cost barrier.
Why does the browser show a padlock — what has it actually verified?
The padlock means: (1) the connection to this server is encrypted, and (2) the server presented a TLS certificate for this exact domain that was signed by a Certificate Authority your browser trusts. It does NOT mean the website is safe, legitimate, or not a phishing site. Attackers can obtain valid TLS certificates for fake domains. The padlock verifies the domain, not the business or person behind it.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.