HTTP vs HTTPS differences every API developer needs to understand
Quick answer
💡HTTP sends data as plaintext TCP. HTTPS adds TLS encryption, which means the server must present a valid certificate, the client validates the certificate chain, and all data is encrypted in transit. For APIs, this matters because browsers block mixed content, Authorization headers are dropped on HTTP redirects, and Secure cookies are never sent over plain HTTP. Use /tools/http-request-builder to test both schemes and observe the difference in response headers.
Error symptoms
- ✕
Mixed Content blocked in browser console when an HTTPS page loads HTTP resources - ✕
TypeError: Failed to fetch on requests from a secure context to a plain HTTP endpoint - ✕
certificate verify failed or CERTIFICATE_VERIFY_FAILED in Python or curl - ✕
Authorization header missing on the request that reaches the server after a redirect - ✕
Secure cookie not sent in the request despite being set correctly - ✕
NET::ERR_CERT_AUTHORITY_INVALID in the browser when using a self-signed certificate
Common causes
- •API base URL left as http:// in environment configuration after migration to HTTPS
- •Self-signed certificate used in development without adding it to the local trust store
- •301 redirect from HTTP to HTTPS stripping the Authorization header before the final request
- •CORS origin mismatch because http://api.example.com and https://api.example.com are different origins
- •Proxy terminates TLS and forwards plain HTTP internally without setting X-Forwarded-Proto
- •HSTS preload list causing browsers to refuse HTTP even before the first redirect
When it happens
- •Migrating an existing API from HTTP to HTTPS
- •Debugging fetch or axios calls from an HTTPS frontend to a newly deployed backend
- •Testing with curl against a staging server that uses a self-signed certificate
- •Implementing OAuth where the callback URL scheme must match exactly
- •Deploying behind a load balancer or reverse proxy that terminates TLS
Examples and fixes
curl, JavaScript fetch, and Python requests showing the scheme difference and header behavior.
API call over HTTP vs HTTPS with credential headers
❌ Wrong
# curl — plain HTTP, Authorization header visible in transit
curl -i http://api.example.test/v1/profile \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
// JS — fetch from HTTPS page to HTTP endpoint (mixed content)
const res = await fetch("http://api.example.test/v1/profile", {
headers: { Authorization: "Bearer YOUR_TOKEN_HERE" }
});
# Python — skipping TLS verification (dangerous)
import requests
r = requests.get("https://api.example.test/v1/profile", verify=False)✅ Fixed
# curl — HTTPS, use -v to observe TLS handshake
curl -i https://api.example.test/v1/profile \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# Add -v to see the certificate chain and TLS version negotiated
// JS — consistent HTTPS; check status before parsing
const res = await fetch("https://api.example.test/v1/profile", {
headers: { Authorization: "Bearer YOUR_TOKEN_HERE" }
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const profile = await res.json();
# Python — always verify TLS; use mkcert for local dev
import requests
r = requests.get("https://api.example.test/v1/profile", timeout=10)
r.raise_for_status()
profile = r.json()The broken curl example uses plain HTTP, meaning the Authorization token travels unencrypted. Any network observer between the client and server can read it. The broken JavaScript fetch makes a mixed content request from an HTTPS page, which browsers block without any response. The broken Python example disables TLS verification with verify=False, which silences the error but leaves the connection vulnerable to MITM attacks even over HTTPS. The fixed versions use HTTPS consistently. The curl version uses -v to expose the TLS handshake details. The Python version raises on non-200 responses and uses a timeout to avoid hanging connections.
A 301 redirect strips the Authorization header. Detect and fix it.
Authorization header dropped on HTTP-to-HTTPS redirect
❌ Wrong
# curl — follows redirect but loses Authorization
curl -L http://api.example.test/v1/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# The 301 redirects to https:// but curl drops Authorization on redirect
// JS — fetch does not follow redirects with credentials by default
const res = await fetch("http://api.example.test/v1/users", {
redirect: "follow",
headers: { Authorization: "Bearer YOUR_TOKEN_HERE" }
});
// Authorization is dropped after the 301✅ Fixed
# curl — target HTTPS directly; avoid the redirect
curl -i https://api.example.test/v1/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
# If you must follow a redirect, use --location-trusted to preserve headers
# (only safe when you trust the redirect destination)
// JS — use the HTTPS URL directly to avoid redirect header stripping
const BASE_URL = process.env.API_BASE_URL; // must be https://
const res = await fetch(`${BASE_URL}/v1/users`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`${res.status}`);
const users = await res.json();
// Server — send 301 only from HTTP, never from HTTPS
app.use((req, res, next) => {
if (!req.secure && req.get("x-forwarded-proto") !== "https") {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});RFC 9110 specifies that a user agent must not automatically send authorization information to a different authority on redirect. When curl follows a 301 from http:// to https://, it treats the new URL as a different authority and drops the Authorization header. The server receives an unauthenticated request and returns 401. The cleanest fix is to use the HTTPS URL directly so no redirect occurs. The server-side middleware checks x-forwarded-proto to handle TLS-terminating proxies where req.secure would always be false.
How TLS turns HTTP into HTTPS
HTTP is a plaintext application protocol that runs over TCP. Every byte of a request, including the URL, headers, and body, travels across the network in a form that any router, ISP, or observer on the same network segment can read. For APIs that carry Authorization headers, session tokens, or user data, this is a meaningful security risk on any network that is not fully trusted.
HTTPS wraps HTTP inside TLS, the Transport Layer Security protocol. Before any HTTP request is sent, TLS performs a handshake. The client sends a ClientHello message listing the TLS versions and cipher suites it supports. The server responds with ServerHello, selects a cipher suite, and presents its certificate. The client validates the certificate chain: the server certificate must be signed by an intermediate CA, which must be signed by a root CA that the client's trust store recognizes. After validation, both parties exchange keys, derive the session encryption keys, and send Finished messages. Only then does the first HTTP request travel across the wire, fully encrypted.
TLS 1.3, published in 2018 and now the dominant version in production, reduces this to a single round trip. TLS 1.2 required two round trips. The practical difference is roughly 50 to 100 milliseconds on a typical cross-continental connection, which is why migrating to TLS 1.3 is worth doing for latency-sensitive APIs.
For API developers, the certificate chain is the most common source of confusion. A certificate chain has three levels: the leaf certificate installed on your server, one or more intermediate CA certificates, and the root CA certificate. If your server does not send the full chain including the intermediates, some clients fail with certificate verify failed even when the leaf certificate is valid. Nginx and Apache require explicit configuration to include the full chain. The openssl s_client -connect hostname:443 command shows exactly what your server is sending.
The difference between HTTP and HTTPS is not just encryption. HTTP/2, which provides multiplexing and header compression that significantly improve API performance, is effectively HTTPS-only in browsers. No browser implements h2 over plaintext HTTP. HTTP/3, which uses QUIC instead of TCP, also encrypts all traffic by design. Choosing HTTP means opting out of every protocol improvement introduced in the last decade.
Mixed content is the browser enforcement mechanism that prevents HTTPS pages from making HTTP requests. Active mixed content, which includes scripts, stylesheets, and fetch requests, is blocked outright. Passive mixed content, which includes images and video, triggers a warning. For API calls from browser applications, any HTTP endpoint will fail silently with a network error when the page is served over HTTPS.
Diagnosing certificate and redirect failures
The fastest diagnostic tool for HTTPS issues is curl -v, which prints the full TLS handshake alongside the HTTP exchange. When a certificate is invalid, curl reports the specific failure: SSL certificate problem: unable to get local issuer certificate usually means the server sent only the leaf certificate and omitted the intermediate CA. CERTIFICATE_VERIFY_FAILED in Python's requests library means the same thing.
Run openssl s_client -connect hostname:443 -showcerts to see every certificate in the chain your server is sending. Paste the leaf certificate into /tools/http-request-builder or an online decoder to check the expiry date, the Subject Alternative Names, and whether the hostname matches. A mismatch between the CN or SANs and the actual hostname triggers NET::ERR_CERT_COMMON_NAME_INVALID in Chrome.
For mixed content errors in browsers, open DevTools, go to the Console tab, and look for messages that say Mixed Content: The page was loaded over HTTPS, but requested an insecure resource. The message includes the exact URL that was blocked. Find that URL in your environment configuration, API client initialization, or hardcoded string and update it to HTTPS.
For Authorization header stripping on redirects, use curl -v -L with your HTTP URL. In the verbose output, look for two requests. The first will show the 301 response. The second request, sent to the redirect target, should not include the Authorization header. This confirms the stripping behavior. The fix is always to use the HTTPS URL directly rather than relying on the redirect.
For CORS failures after migration, remember that http://api.example.com and https://api.example.com are different origins. If your backend's Access-Control-Allow-Origin includes the HTTP origin but not the HTTPS origin, or vice versa, the preflight will fail after migration. Test this with curl -X OPTIONS on both origins.
For proxy-based TLS termination issues, check whether your backend code reads req.secure or req.protocol to determine the scheme. Behind a load balancer, both will always be false and http respectively because the proxy handles TLS and forwards plain HTTP. The proxy should set X-Forwarded-Proto: https, and your application should trust that header to determine the original scheme.
Fixing scheme mismatches and certificate errors
The most common fix is changing every hardcoded http:// to https:// in environment variables, API client configuration, and .env files. Use a global search across the repository rather than fixing individual files. Pay attention to values in docker-compose.yml, Kubernetes ConfigMaps, and CI/CD pipeline variables because these are frequently overlooked.
For development environments, use mkcert to create locally trusted certificates instead of disabling TLS verification. Run mkcert -install once to add the mkcert root CA to your system trust store, then mkcert localhost 127.0.0.1 to generate a certificate pair. Configure your development server to use these files. Browsers and curl will trust the certificate without any -k or verify=False flags. This keeps development behavior identical to production TLS behavior.
For self-signed certificates in CI or container environments, add the CA certificate to the system trust store rather than disabling verification. In Alpine Linux containers, copy the CA file to /usr/local/share/ca-certificates/ and run update-ca-certificates. In Python, set the SSL_CERT_FILE environment variable to point to the CA bundle file. Disabling verification with verify=False or CURL_SSL_NOVERIFY should never reach production.
For the Authorization header stripping problem, configure your application to always use the final HTTPS URL rather than the HTTP URL that redirects. Store the API base URL in an environment variable and enforce the https:// scheme at startup. Add a validation check that raises an error if the configured URL uses http:// in production environments.
For TLS-terminating proxies, configure the application to trust X-Forwarded-Proto: https and generate redirect URLs based on that header. In Express, set app.set("trust proxy", 1) and then use req.secure which will correctly reflect the original client scheme. In Django, set SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https").
For HSTS, add the Strict-Transport-Security header with a long max-age after confirming that HTTPS works reliably. Start with max-age=86400 for one day of testing, then increase to max-age=31536000 for one year. Include the includeSubDomains directive only after confirming all subdomains serve HTTPS. Use /tools/http-request-builder to verify the HSTS header is present in responses.
Proxy, HSTS, and dev certificate gotchas
TLS-terminating load balancers create a specific class of problems where the backend application always sees plain HTTP connections regardless of what the client sent. If your application generates absolute redirect URLs using req.protocol or the Host header, it will produce http:// URLs even for clients that connected over HTTPS. The server-side fix is to trust X-Forwarded-Proto and derive the URL scheme from that header rather than the connection type.
HSTS preloading adds a browser-enforced guarantee that can be very difficult to reverse. When a site is added to the HSTS preload list embedded in browsers, all requests to that domain use HTTPS regardless of whether the server sends an HSTS header. If the HTTPS certificate later expires or is misconfigured, users cannot access the site through HTTP as a fallback. Preloading should only happen after HTTPS is stable for the entire domain and all subdomains.
Let's Encrypt certificates renew every 90 days. If the renewal process fails silently, certificates expire and all clients receive certificate errors simultaneously. Use a monitoring tool that alerts on certificate expiry at 30 days, 14 days, and 7 days before expiration. The Certbot renewal process can be tested with certbot renew --dry-run.
HTTP/2 multiplexes multiple requests over a single TLS connection, which changes the behavior of rate limit counters that are based on connection counts. API gateways that apply per-connection rate limits may behave unexpectedly when clients switch from HTTP/1.1 to HTTP/2.
OAuth redirect URIs must match exactly including the scheme. If your OAuth application is registered with an HTTP callback URL and the redirect comes from an HTTPS page, the authorization server rejects it. Update the registered URI in the OAuth provider dashboard when migrating to HTTPS. Google, GitHub, and most providers perform exact string matching on redirect URIs.
In Python's requests library, the default CA bundle comes from the certifi package. Some corporate environments replace system CA certificates with a corporate root CA that certifi does not include. The fix is to set the REQUESTS_CA_BUNDLE environment variable to point to the corporate CA bundle file.
Mistakes that introduce security regressions
Disabling TLS verification to silence a certificate error is the most harmful workaround because it is often treated as temporary and then left indefinitely. requests.get(url, verify=False) or curl -k removes the protection that TLS provides. An encrypted connection where the certificate is not verified does not protect against man-in-the-middle attacks because the attacker can present any certificate. The correct fix is always to make the certificate trusted, not to disable verification.
Sending API credentials over HTTP in development and assuming production will be different creates a habit of exposing secrets. Developers who test with HTTP endpoints during development sometimes forget that credentials sent over HTTP are logged in plaintext in proxy access logs, network captures, and load balancer logs. Use HTTPS even in staging environments.
Using 302 Found for the HTTP-to-HTTPS redirect instead of 301 Moved Permanently means browsers and intermediaries do not cache the redirect. Clients repeat the HTTP request on every visit before following the redirect to HTTPS. Use 301 for the HTTP-to-HTTPS redirect specifically. During testing, if you are not certain about the final URL, use 307 Temporary Redirect, which preserves the HTTP method and does not get cached.
Forgetting to add the intermediate CA certificate to the server configuration is a common deployment mistake. The server certificate alone is not sufficient. Clients that do not have the intermediate CA in their local cache fail to build the trust chain. Include the full certificate chain in your server configuration file, ordered from leaf to root.
Relying on curl -k to test an API and then assuming the same behavior from browsers or Python is incorrect. curl with -k accepts any certificate, including expired or self-signed ones. Browsers apply stricter policies and will block connections that curl accepts. Always verify with the same client type that your users actually use.
Enforcing HTTPS across the full stack
Enforce HTTPS at every layer of the stack rather than relying on any single enforcement point. The web server or load balancer should redirect HTTP to HTTPS with a 301. The application should include Strict-Transport-Security in responses. Environment configurations should store only HTTPS URLs. API clients should validate the scheme at initialization time. Defense in depth means a misconfiguration in one layer does not create a plaintext gap.
Use mkcert for all local development HTTPS instead of self-signed certificates or disabled verification. The mkcert workflow takes two commands and produces certificates that browsers, curl, and Python all trust without special flags. It also creates consistent behavior between development and production by keeping TLS verification active throughout the development lifecycle.
Monitor certificate expiry proactively. A Let's Encrypt certificate expires every 90 days. If the automated renewal cron job fails, you have a maximum of 90 days before all clients receive certificate errors. Set up monitoring that alerts at 30, 14, and 7 days before expiration. The Certbot team recommends running renewal twice per day so that a transient failure is automatically retried before the expiry window closes.
For API clients that call third-party HTTPS endpoints, pin the expected certificate or CA in security-sensitive integrations. Certificate pinning prevents an attacker who compromises a certificate authority from issuing a fraudulent certificate for your API endpoint. Most HTTP client libraries support CA pinning without full certificate pinning, which balances security and maintainability.
Test HTTPS behavior using /tools/http-request-builder to send requests to your endpoint and inspect the response headers including Strict-Transport-Security, Content-Security-Policy, and the TLS version negotiated. Compare headers between staging and production to catch configuration drift before it affects users.
Document the HTTPS configuration for your team: which CA issues your certificate, when it expires, how renewal is automated, which cipher suites are enabled, and what TLS version is required. When a new engineer provisions a service or rotates a certificate, they should have a clear reference rather than discovering requirements through failure.
HTTPS migration checklist
- ✓Update all environment variables and configuration files to use https:// URLs.
- ✓Configure your server to send the full certificate chain including intermediates.
- ✓Add a 301 redirect from HTTP to HTTPS at the load balancer or web server level.
- ✓Set Strict-Transport-Security with a short max-age initially and increase after confirming stability.
- ✓Test with curl -v to verify the TLS handshake, certificate chain, and response headers.
- ✓Update CORS Access-Control-Allow-Origin to include the HTTPS origin after migration.
- ✓Update OAuth redirect URIs in all provider dashboards to use HTTPS.
- ✓Set up certificate expiry monitoring at 30, 14, and 7 days before the expiry date.
Related guides
Frequently asked questions
What is the main security difference between HTTP and HTTPS?
HTTP sends all data including headers, cookies, and request bodies as plaintext over TCP. Any network observer between the client and server can read and modify it. HTTPS encrypts everything using TLS after a handshake that verifies the server's certificate. This prevents eavesdropping and man-in-the-middle attacks. For APIs carrying authorization tokens, HTTPS is mandatory, not optional.
Why does my Authorization header disappear after an HTTP-to-HTTPS redirect?
RFC 9110 requires HTTP clients to drop the Authorization header when following a redirect to a different authority, which includes a different scheme. A 301 from http:// to https:// counts as a different authority, so the header is stripped before the redirected request. The fix is to use the HTTPS URL directly in your client configuration, eliminating the redirect entirely.
What is mixed content and why does it block my API calls?
Mixed content occurs when an HTTPS page makes requests to HTTP URLs. Browsers block active mixed content, which includes fetch requests and XHR, because an attacker controlling the HTTP resource could inject malicious code into the secure page. The browser blocks the request before it reaches the network, producing a TypeError: Failed to fetch with no response to inspect.
How do I use HTTPS in local development without disabling TLS verification?
Use mkcert, which creates locally trusted certificates signed by a CA that the tool installs into your system trust store. Run mkcert -install once, then mkcert localhost 127.0.0.1 to generate the certificate files. Configure your dev server to use those files. Browsers and command-line tools including curl and Python requests will trust the certificate without any special flags.
What is HSTS and should I enable it for my API?
HTTP Strict Transport Security (HSTS) is a response header, Strict-Transport-Security: max-age=31536000, that tells browsers to always use HTTPS for your domain for the specified duration. Enabling it prevents protocol downgrade attacks. For APIs, enable HSTS after confirming HTTPS is fully stable. Start with a short max-age for testing before increasing to one year.
How does TLS 1.3 differ from TLS 1.2 for API performance?
TLS 1.3 completes the handshake in one round trip rather than two. On a connection with 100ms latency, TLS 1.3 saves approximately 100ms per new connection compared to TLS 1.2. TLS 1.3 also removes weaker cipher suites and improves forward secrecy. For APIs with many short-lived connections, the RTT savings are meaningful. Most modern servers and clients support TLS 1.3 by default.
Why does curl succeed but the browser fails with a certificate error?
curl by default uses the system CA bundle, while browsers use their own built-in trust store. A certificate signed by a CA that is in the system bundle but not in the browser's bundle will be trusted by curl and rejected by the browser. The reverse also applies. Always test with the same client type that your actual users employ. Browser trust store updates happen through browser releases, not OS updates.
What certificate should I use for production APIs?
Use a certificate from a publicly trusted CA. Let's Encrypt provides free domain-validated certificates that renew every 90 days and are trusted by all major browsers and runtimes. For APIs requiring organization validation or wildcard coverage, commercial CAs offer more options. Self-signed certificates should never be used in production because they require every client to explicitly trust them.
Does HTTP/2 require HTTPS?
Technically no — the HTTP/2 specification allows h2c, which is HTTP/2 over plain TCP. In practice, every major browser implements HTTP/2 only over TLS. If your API is browser-facing, HTTP/2 requires HTTPS. For server-to-server communication where you control both endpoints, h2c is possible but uncommon and not recommended due to the loss of TLS protections.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.