A command-line HTTP request gives a small, inspectable view of a service. It can show the response status, headers, body, and timing without a browser’s rendering layer. Start with a concrete question: whether the original URL redirects correctly, whether the final representation is available, or whether a particular method is accepted. Those questions require different probes. One successful curl invocation does not establish that every part of a website works.

Separate four kinds of evidence

By default, curl writes the response body and can exit successfully even when HTTP reports an error. The process exit status describes curl’s transfer outcome, while the HTTP status describes the received response. Keep both. A body that says “missing” with status 404 differs from a connection failure that produced no HTTP response at all. A monitoring script should test its intended response contract explicitly.

Use -D to capture response headers, -o to choose where transfer output goes, and -w to print selected metadata separately. -sS hides the progress meter while retaining error messages. --max-time bounds the transfer, and redirect-following probes should also bound the number of hops. Record the options alongside results so a later comparison uses the same method, URL, and request headers.

Timing fields depend on connection reuse, proxy routing, and redirects. A reused connection can avoid a fresh DNS lookup. Compare equivalent conditions before treating a lower lookup time as evidence that the DNS service improved.

When it breaks

Adding -L immediately can hide a wrong first hop behind a healthy final page. Inspect the original status and Location when debugging routing, then follow deliberately when testing the destination. A redirect loop, an unexpected host, or a method change can matter even when some later page returns 200. Following a chain is not universally the right definition of success.

-I sends HEAD; it is not a GET whose body curl merely discards. A server, proxy, or authorization rule may handle HEAD differently. If a HEAD probe fails while GET works, inspect that method difference before declaring the resource unavailable. Likewise, changing a User-Agent header does not turn curl into a browser: JavaScript, layout, browser cookie rules, and CORS enforcement require their own checks.

Verbose output can contain credentials and private request details. Capture only the diagnostic fields needed and keep secrets out of commands and saved traces. Disabling certificate verification changes the question being tested; it cannot establish that a normal HTTPS client trusts the server. Inherited proxy settings and curl configuration can also change a probe’s route or behavior, so control them when reproducibility matters.

Check it quickly

Save this as curl-probe.py and run python3 curl-probe.py with curl installed. It was checked with curl 8.7.1. The script runs a temporary loopback HTTP server and launches the real curl command. Its first option disables the default curl configuration, and the local requests bypass proxies. No external site or TLS service is involved.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from tempfile import TemporaryDirectory
from threading import Thread
import json
import subprocess


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        status = 302 if self.path == '/move' else 200 if self.path == '/ok' else 404
        data = b'healthy\n' if status == 200 else b'missing\n' if status == 404 else b''
        self.send_response(status)
        if status == 302:
            self.send_header('Location', '/ok')
        self.send_header('Content-Length', str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_HEAD(self):
        self.send_response(405)
        self.send_header('Allow', 'GET')
        self.send_header('Content-Length', '0')
        self.end_headers()

    def log_message(self, *args):
        pass


server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever)
thread.start()
try:
    origin = f'http://127.0.0.1:{server.server_port}'
    with TemporaryDirectory() as directory:
        root = Path(directory)

        def probe(path, *options):
            body, headers = root / 'body', root / 'headers'
            result = subprocess.run([
                'curl', '-q', '--noproxy', '*', '--max-time', '5', '-sS',
                '-o', str(body), '-D', str(headers), '-w', '%{http_code} %{num_redirects}',
                *options, origin + path], capture_output=True, text=True, timeout=10)
            status, redirects = map(int, result.stdout.split())
            return {'exit': result.returncode, 'status': status, 'redirects': redirects,
                    'output': body.read_text(), 'allow_get': 'Allow: GET' in headers.read_text()}

        results = {'missing_default': probe('/missing'),
                   'missing_fail': probe('/missing', '--fail-with-body'),
                   'redirect_inspect': probe('/move'),
                   'redirect_follow': probe('/move', '--location', '--max-redirs', '2'),
                   'head': probe('/ok', '--head'), 'get': probe('/ok')}
        assert results['missing_default']['exit'] == 0
        assert results['missing_fail']['exit'] == 22
        assert results['missing_fail']['output'] == 'missing\n'
        assert (results['redirect_inspect']['status'], results['redirect_inspect']['redirects']) == (302, 0)
        assert (results['redirect_follow']['status'], results['redirect_follow']['redirects']) == (200, 1)
        assert results['head']['status'] == 405 and results['head']['allow_get']
        assert results['get']['status'] == 200 and results['get']['output'] == 'healthy\n'
        print(json.dumps(results))
finally:
    server.shutdown()
    thread.join()
    server.server_close()

The ordinary missing-page transfer returns HTTP 404 with process exit zero. Adding --fail-with-body changes the exit to 22 while preserving the diagnostic body. This directly demonstrates why checking only the process status misses some HTTP failures. The option requires a curl version that supports it; inspect the installed manual when adapting the command.

The redirect inspection reports 302 with zero followed hops. Explicit following reaches 200 after one hop. The same /ok resource returns 405 with Allow: GET to HEAD and 200 with healthy to GET. For HEAD, curl’s captured output contains headers rather than a representation body, which is why the result field is named output.

These cases verify transfer classification, redirect choice, and method differences. They do not test TLS, browser behavior, latency under load, or the application’s downstream dependencies. Turn a useful manual probe into an automated check only after specifying the expected status, relevant headers, body condition, and timeout. Otherwise automation simply repeats an ambiguous question more frequently.

Further reading

Sources

  • curl manual, exit behavior, headers, redirects, and bounded transfers, checked 2026-09-13.
  • RFC 9110: HTTP Semantics, methods and response semantics interpreted by the probe, checked 2026-09-13.