Skip to main content

HTTP: The Protocol That Carries Your GraphQL (Whether It Likes It or Not)

· 14 min read
GraphQL Guy

HTTP Protocol

Every GraphQL query you send travels over HTTP. Every response comes back the same way. But here's the thing: HTTP was designed for fetching documents, not executing queries. GraphQL essentially hijacked a delivery truck and turned it into a taxi service. Let's explore the protocol that makes it all possible.

The Origin Story

HTTP - Hypertext Transfer Protocol - was invented by Tim Berners-Lee in 1989 at CERN. The original goal? Link scientific documents together so physicists could share research. Fast forward 35 years, and it's carrying cat videos, financial transactions, and yes, your carefully crafted GraphQL mutations.

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP TIMELINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1989 Tim Berners-Lee proposes the World Wide Web at CERN │
│ 1991 HTTP/0.9 - One-line protocol, GET only, no headers │
│ 1996 HTTP/1.0 - RFC 1945, added headers, POST, status codes │
│ 1997 HTTP/1.1 - RFC 2068, persistent connections, chunked │
│ 1999 HTTP/1.1 - RFC 2616, the definitive version for 15 years │
│ 2014 HTTP/1.1 - RFC 7230-7235, clarified and split into parts │
│ 2015 HTTP/2 - RFC 7540, binary framing, multiplexing │
│ 2022 HTTP/3 - RFC 9114, QUIC-based, UDP instead of TCP │
│ │
│ Today: Still the backbone of the web, now in its third major │
│ version, carrying everything from HTML to GraphQL │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP/0.9: The Innocent Beginning

The original HTTP was adorably simple. The entire protocol was one line:

GET /page.html

That's it. No headers. No status codes. No content types. The server would respond with raw HTML and close the connection. Done.

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP/0.9 CONVERSATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Client ──▶ GET /hello.html │
│ │
│ Server ──▶ <html> │
│ <body>Hello World</body> │
│ </html> │
│ [connection closed] │
│ │
│ No status codes. No headers. Pure document transfer. │
│ │
└─────────────────────────────────────────────────────────────────────┘

Could you run GraphQL over HTTP/0.9? Technically yes. Would it be a nightmare? Absolutely.

HTTP/1.0: Growing Up

HTTP/1.0 introduced the concepts we still use today:

GET /movie/123 HTTP/1.0
Host: api.example.com
Accept: application/json
User-Agent: GraphQLClient/1.0

HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 127

{"data":{"movie":{"id":"123","title":"The Matrix"}}}

The Request Anatomy

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP REQUEST STRUCTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ POST /graphql HTTP/1.1 │ ◀─ Request Line
│ ├─────────────────────────────────────────────────────────┤ │
│ │ Host: api.example.com │ │
│ │ Content-Type: application/json │ ◀─ Headers
│ │ Authorization: Bearer eyJhbGc... │ │
│ │ Content-Length: 89 │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ │ ◀─ Blank Line
│ ├─────────────────────────────────────────────────────────┤ │
│ │ {"query":"{ movie(id: \"1\") { title } }"} │ ◀─ Body
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP Methods: The Verbs

HTTP defines methods (verbs) for different operations:

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP METHODS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Method Idempotent Safe Body Purpose │
│ ─────────────────────────────────────────────────────────────── │
│ GET Yes Yes No* Retrieve resource │
│ HEAD Yes Yes No Get headers only │
│ POST No No Yes Create/submit data │
│ PUT Yes No Yes Replace resource │
│ PATCH No No Yes Partial update │
│ DELETE Yes No No* Remove resource │
│ OPTIONS Yes Yes No Get allowed methods │
│ TRACE Yes Yes No Debug/echo request │
│ CONNECT No No Yes Establish tunnel │
│ │
│ * Technically allowed but rarely used │
│ │
│ GraphQL uses: POST (always), GET (queries only, optional) │
│ │
└─────────────────────────────────────────────────────────────────────┘

GraphQL's controversial choice: GraphQL uses POST for everything - queries, mutations, subscriptions. This violates REST conventions where GET should be used for reads. But GraphQL has good reasons:

  1. Query strings have length limits (~2000-8000 chars depending on browser/server)
  2. GraphQL queries can be massive (nested selections, fragments, variables)
  3. Caching is handled differently anyway (operation-level, not URL-level)

Status Codes: How HTTP Talks Back

HTTP status codes are three-digit numbers that tell you what happened:

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP STATUS CODE FAMILIES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1xx - Informational (request received, continuing) │
│ 100 Continue "Keep sending that body" │
│ 101 Switching "Upgrading to WebSocket now" │
│ │
│ 2xx - Success (request received, understood, accepted) │
│ 200 OK "Here's your data" │
│ 201 Created "Resource created successfully" │
│ 204 No Content "Done, nothing to return" │
│ │
│ 3xx - Redirection (further action needed) │
│ 301 Moved "Permanently moved, update your links" │
│ 302 Found "Temporarily over there" │
│ 304 Not Modified "Use your cached version" │
│ │
│ 4xx - Client Error (you messed up) │
│ 400 Bad Request "I can't understand your request" │
│ 401 Unauthorized "Who are you?" │
│ 403 Forbidden "I know who you are, and no" │
│ 404 Not Found "That doesn't exist" │
│ 429 Too Many "Slow down there, buddy" │
│ │
│ 5xx - Server Error (we messed up) │
│ 500 Internal Error "Something broke" │
│ 502 Bad Gateway "Upstream server failed" │
│ 503 Unavailable "Server is overloaded or down" │
│ 504 Gateway Timeout "Upstream took too long" │
│ │
└─────────────────────────────────────────────────────────────────────┘

GraphQL's Status Code Philosophy

Here's where GraphQL gets weird. A GraphQL response almost always returns 200 OK, even when there are errors:

HTTP/1.1 200 OK
Content-Type: application/json

{
"data": null,
"errors": [
{
"message": "User not found",
"path": ["user"]
}
]
}

Wait, what? The user wasn't found but we got 200 OK?

GraphQL's reasoning:

  • The HTTP request succeeded (it reached the server, was parsed, was executed)
  • The GraphQL execution had errors, but that's GraphQL-level, not HTTP-level
  • Partial success is possible (some fields resolve, others fail)
┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL HTTP STATUS CODES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ HTTP Code When GraphQL Response │
│ ─────────────────────────────────────────────────────────────── │
│ 200 OK Query executed (even with errors) { data, errors } │
│ 400 Malformed JSON or invalid query { errors } │
│ 401 Authentication required (varies) │
│ 405 Wrong method (GET for mutation) (varies) │
│ 500 Server crashed during execution { errors } │
│ │
│ Note: Most implementations return 200 for everything that │
│ executes, reserving 4xx/5xx for transport-level failures. │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP Headers: The Metadata Layer

Headers are key-value pairs that provide metadata about the request or response.

Essential Request Headers

POST /graphql HTTP/1.1
Host: api.movies.com # Required in HTTP/1.1
Content-Type: application/json # What format is the body?
Content-Length: 156 # How big is the body?
Accept: application/json # What format do you want back?
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: MyApp/1.0 # Who's calling?
Accept-Encoding: gzip, deflate # Compression we understand
Connection: keep-alive # Don't close after response

Essential Response Headers

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1234
Content-Encoding: gzip # Response is compressed
Cache-Control: no-store # Don't cache this
Date: Mon, 24 Jun 2024 10:30:00 GMT
X-Request-Id: abc123 # For debugging/tracing

Headers That Matter for GraphQL

┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL-RELEVANT HEADERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ REQUEST HEADERS: │
│ ──────────────── │
│ Content-Type: application/json # Standard for GraphQL │
│ Content-Type: application/graphql # Alternative (query in body) │
│ Authorization: Bearer <token> # Auth token │
│ X-Request-ID: <uuid> # Request tracing │
│ Apollo-Require-Preflight: true # Apollo Client specific │
│ │
│ RESPONSE HEADERS: │
│ ───────────────── │
│ Content-Type: application/json # Always JSON │
│ Cache-Control: no-store # Usually no caching │
│ X-Cache: HIT/MISS # CDN cache status │
│ Retry-After: 60 # Rate limiting hint │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP/1.1: The Workhorse

HTTP/1.1 (1997-2015) was the dominant protocol version for nearly two decades. Key features:

Persistent Connections

HTTP/1.0 closed the connection after each request. HTTP/1.1 keeps it open:

┌─────────────────────────────────────────────────────────────────────┐
│ CONNECTION COMPARISON │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ HTTP/1.0 (one request per connection): │
│ │
│ Client Server │
│ │─TCP──▶│ Connect │
│ │─GET──▶│ Request │
│ │◀─200──│ Response │
│ │◀─FIN──│ Close │
│ │─TCP──▶│ Connect (again!) │
│ │─GET──▶│ Request │
│ │◀─200──│ Response │
│ │◀─FIN──│ Close (again!) │
│ │
│ HTTP/1.1 (persistent connection): │
│ │
│ Client Server │
│ │─TCP──▶│ Connect (once) │
│ │─GET──▶│ Request 1 │
│ │◀─200──│ Response 1 │
│ │─GET──▶│ Request 2 (same connection!) │
│ │◀─200──│ Response 2 │
│ │─GET──▶│ Request 3 │
│ │◀─200──│ Response 3 │
│ │◀─FIN──│ Close (eventually) │
│ │
└─────────────────────────────────────────────────────────────────────┘

Chunked Transfer Encoding

Don't know the size upfront? Stream it:

HTTP/1.1 200 OK
Transfer-Encoding: chunked

7\r\n
{"data"\r\n
5\r\n
:{"m\r\n
8\r\n
ovie":{}\r\n
2\r\n
}}\r\n
0\r\n
\r\n

This is crucial for GraphQL subscriptions and streaming responses (like @defer and @stream).

The Head-of-Line Blocking Problem

HTTP/1.1's fatal flaw: requests must be processed in order:

┌─────────────────────────────────────────────────────────────────────┐
│ HEAD-OF-LINE BLOCKING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Client sends: [Request A] [Request B] [Request C] │
│ ↓ ↓ ↓ │
│ Server must: Process A then B then C │
│ (slow!) │
│ ↓ ↓ ↓ │
│ Client gets: [Response A] [Response B] [Response C] │
│ │
│ If Request A is slow (complex query), B and C wait! │
│ │
│ Workaround: Open multiple TCP connections (typically 6 per host) │
│ But: More connections = more overhead = more latency │
│ │
└─────────────────────────────────────────────────────────────────────┘

For GraphQL, this means a slow query blocks subsequent queries on the same connection.

HTTP/2: The Multiplexing Revolution

HTTP/2 (2015) fixed head-of-line blocking with multiplexing:

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP/2 MULTIPLEXING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Single TCP Connection, Multiple Streams: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Stream 1: [Frame][Frame][Frame]────────▶ Response A │ │
│ │ Stream 3: [Frame][Frame]───────────────▶ Response B │ │
│ │ Stream 5: [Frame][Frame][Frame][Frame]─▶ Response C │ │
│ │ Stream 7: [Frame]──────────────────────▶ Response D │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ All happening simultaneously on ONE connection! │
│ │
│ Key Features: │
│ • Binary framing (more efficient than text) │
│ • Header compression (HPACK) │
│ • Server push (preemptively send resources) │
│ • Stream prioritization │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP/2 Frame Types

HTTP/2 is binary, not text. Messages are split into frames:

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP/2 FRAME STRUCTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ +-----------------------------------------------+ │
│ | Length (24 bits) | │
│ +---------------+---------------+---------------+ │
│ | Type (8) | Flags (8) | | │
│ +---------------+---------------+---------------+ │
│ |R| Stream Identifier (31 bits) | │
│ +-----------------------------------------------+ │
│ | Payload | │
│ +-----------------------------------------------+ │
│ │
│ Frame Types: │
│ 0x0 DATA - Request/response body │
│ 0x1 HEADERS - HTTP headers │
│ 0x2 PRIORITY - Stream priority │
│ 0x3 RST_STREAM - Cancel a stream │
│ 0x4 SETTINGS - Connection settings │
│ 0x5 PUSH_PROMISE - Server push │
│ 0x6 PING - Keep-alive/latency measurement │
│ 0x7 GOAWAY - Graceful shutdown │
│ 0x8 WINDOW_UPDATE - Flow control │
│ 0x9 CONTINUATION - Header continuation │
│ │
└─────────────────────────────────────────────────────────────────────┘

GraphQL Benefits from HTTP/2

┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL + HTTP/2 BENEFITS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Scenario: Dashboard loading 5 GraphQL queries simultaneously │
│ │
│ HTTP/1.1: │
│ • Open 5 connections (or queue on fewer) │
│ • Each connection has TCP handshake overhead │
│ • Slow query blocks others on same connection │
│ │
│ HTTP/2: │
│ • Single connection, 5 streams │
│ • One TCP handshake total │
│ • Queries complete independently │
│ • Header compression reduces redundant data │
│ │
│ Result: Faster perceived performance, less server load │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP/3: The QUIC Revolution

HTTP/3 (2022) replaces TCP with QUIC (UDP-based):

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP VERSION COMPARISON │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Feature HTTP/1.1 HTTP/2 HTTP/3 │
│ ───────────────────────────────────────────────────────────────── │
│ Transport TCP TCP QUIC (UDP) │
│ Multiplexing No Yes Yes │
│ Header Compress No HPACK QPACK │
│ Encryption Optional Optional* Mandatory │
│ HOL Blocking Yes TCP-level No │
│ Connection Setup 1-3 RTT 1-3 RTT 0-1 RTT │
│ Connection Mig. No No Yes │
│ │
│ * Browsers require HTTPS for HTTP/2 │
│ │
└─────────────────────────────────────────────────────────────────────┘

Why QUIC Matters

HTTP/2 solved application-level head-of-line blocking but TCP still has it:

┌─────────────────────────────────────────────────────────────────────┐
│ TCP HEAD-OF-LINE BLOCKING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ HTTP/2 over TCP (packet loss scenario): │
│ │
│ Stream 1: [Packet 1] [Packet 2] [Packet 3] │
│ Stream 2: [Packet 4] [Packet 5] │
│ Stream 3: [Packet 6] [Packet 7] [Packet 8] │
│ │
│ If Packet 4 is lost: │
│ TCP blocks ALL streams until Packet 4 is retransmitted! │
│ Streams 1 and 3 wait for Stream 2's lost packet. │
│ │
│ HTTP/3 over QUIC: │
│ Each stream is independent at the transport level. │
│ Packet loss on Stream 2 only affects Stream 2. │
│ │
└─────────────────────────────────────────────────────────────────────┘

Zero Round Trip Connection (0-RTT)

┌─────────────────────────────────────────────────────────────────────┐
│ CONNECTION SETUP COMPARISON │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ TCP + TLS 1.2 (HTTP/1.1, HTTP/2): │
│ │
│ Client Server │
│ │──SYN──────────▶│ 1 RTT (TCP) │
│ │◀─────SYN-ACK───│ │
│ │──ACK──────────▶│ │
│ │──ClientHello──▶│ 2 RTT (TLS) │
│ │◀─ServerHello───│ │
│ │◀─Certificate───│ │
│ │──KeyExchange──▶│ │
│ │──Finished─────▶│ │
│ │◀─Finished──────│ │
│ │──GET /graphql─▶│ Finally! (3 RTT total) │
│ │
│ QUIC (HTTP/3, resumed connection): │
│ │
│ Client Server │
│ │──GET /graphql─▶│ 0 RTT! (using cached keys) │
│ │◀─────200 OK────│ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Content Negotiation

HTTP lets clients and servers negotiate format, language, and encoding:

# Request
GET /graphql HTTP/1.1
Accept: application/json, application/xml;q=0.9, */*;q=0.1
Accept-Language: en-US, en;q=0.9, de;q=0.8
Accept-Encoding: gzip, deflate, br

# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: en-US
Content-Encoding: gzip
Vary: Accept, Accept-Encoding

The q parameter indicates preference (0.0-1.0):

  • application/json - implied q=1.0 (most preferred)
  • application/xml;q=0.9 - second choice
  • */*;q=0.1 - anything else as last resort

Caching: The HTTP Superpower

HTTP has sophisticated caching built in:

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP CACHING HEADERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ RESPONSE HEADERS (server → cache): │
│ ────────────────────────────────── │
│ Cache-Control: max-age=3600 # Cache for 1 hour │
│ Cache-Control: no-cache # Validate before using │
│ Cache-Control: no-store # Never cache │
│ Cache-Control: private # Only browser cache │
│ Cache-Control: public # CDN can cache too │
│ ETag: "abc123" # Content fingerprint │
│ Last-Modified: Mon, 24 Jun 2024... # When content changed │
│ Vary: Accept-Encoding # Cache varies by this header │
│ │
│ REQUEST HEADERS (client → server): │
│ ───────────────────────────────── │
│ If-None-Match: "abc123" # Return 304 if ETag matches │
│ If-Modified-Since: Mon, 24 Jun... # Return 304 if unchanged │
│ Cache-Control: no-cache # Force fresh response │
│ │
└─────────────────────────────────────────────────────────────────────┘

The GraphQL Caching Problem

Here's why GraphQL and HTTP caching don't play nice:

# REST (cacheable by URL)
GET /movies/123 HTTP/1.1
# Cache key: /movies/123
# Easy to cache!

# GraphQL (same URL, different data)
POST /graphql HTTP/1.1

{"query": "{ movie(id: \"123\") { title } }"}

# vs

{"query": "{ movie(id: \"123\") { title director { name } reviews { rating } } }"}

# Same URL, same method, totally different responses!

Solutions:

┌─────────────────────────────────────────────────────────────────────┐
│ GRAPHQL CACHING STRATEGIES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Persisted Queries (Apollo, Relay) │
│ GET /graphql?id=abc123&variables={"id":"1"} │
│ → Now it's cacheable by URL! │
│ │
│ 2. CDN Response Caching (with extensions) │
│ Response includes: { "extensions": { "cacheControl": {...} } } │
│ CDN parses extensions and caches accordingly │
│ │
│ 3. Application-Level Caching │
│ Cache at resolver level, not HTTP level │
│ Use DataLoader for request-scoped caching │
│ Use Redis/Memcached for cross-request caching │
│ │
│ 4. Normalized Client Cache (Apollo Client, Relay) │
│ Client caches entities by ID │
│ Different queries share cached entities │
│ │
└─────────────────────────────────────────────────────────────────────┘

CORS: The Browser's Security Guard

Cross-Origin Resource Sharing controls which websites can call your API:

┌─────────────────────────────────────────────────────────────────────┐
│ CORS FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Browser (myapp.com) wants to call API (api.example.com): │
│ │
│ 1. Preflight Request (for "non-simple" requests): │
│ │
│ OPTIONS /graphql HTTP/1.1 │
│ Origin: https://myapp.com │
│ Access-Control-Request-Method: POST │
│ Access-Control-Request-Headers: Content-Type, Authorization │
│ │
│ 2. Preflight Response: │
│ │
│ HTTP/1.1 204 No Content │
│ Access-Control-Allow-Origin: https://myapp.com │
│ Access-Control-Allow-Methods: POST, GET, OPTIONS │
│ Access-Control-Allow-Headers: Content-Type, Authorization │
│ Access-Control-Max-Age: 86400 │
│ │
│ 3. Actual Request (if preflight passed): │
│ │
│ POST /graphql HTTP/1.1 │
│ Origin: https://myapp.com │
│ Content-Type: application/json │
│ Authorization: Bearer token123 │
│ │
│ 4. Actual Response: │
│ │
│ HTTP/1.1 200 OK │
│ Access-Control-Allow-Origin: https://myapp.com │
│ Content-Type: application/json │
│ │
└─────────────────────────────────────────────────────────────────────┘

GraphQL CORS gotcha: GraphQL always sends Content-Type: application/json, which triggers preflight. Every first request to your API has this overhead.

CORS Configuration Example (Spring)

@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/graphql")
.allowedOrigins("https://myapp.com", "https://admin.myapp.com")
.allowedMethods("POST", "GET", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true)
.maxAge(86400); // Cache preflight for 24 hours
}
}

WebSocket Upgrade: Real-Time GraphQL

GraphQL subscriptions often use WebSocket, which starts as HTTP:

# Upgrade Request
GET /graphql HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: graphql-ws
Sec-WebSocket-Version: 13

# Upgrade Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: graphql-ws

After this handshake, the connection switches from HTTP to WebSocket, and GraphQL subscription messages flow freely.

Security Headers

Beyond authentication, these headers protect your API:

┌─────────────────────────────────────────────────────────────────────┐
│ SECURITY HEADERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Header Purpose │
│ ───────────────────────────────────────────────────────────────── │
│ Strict-Transport-Security Force HTTPS │
│ X-Content-Type-Options Prevent MIME sniffing │
│ X-Frame-Options Prevent clickjacking │
│ Content-Security-Policy Control resource loading │
│ X-XSS-Protection Enable XSS filter (legacy) │
│ Referrer-Policy Control referer header │
│ │
│ Example for GraphQL API: │
│ │
│ Strict-Transport-Security: max-age=31536000; includeSubDomains │
│ X-Content-Type-Options: nosniff │
│ X-Frame-Options: DENY │
│ Content-Security-Policy: default-src 'none'; frame-ancestors 'none'│
│ │
└─────────────────────────────────────────────────────────────────────┘

Debugging HTTP

Browser DevTools

┌─────────────────────────────────────────────────────────────────────┐
│ NETWORK TAB INSIGHTS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Timing Breakdown: │
│ │
│ ├─ Queueing Wait for connection │
│ ├─ Stalled Blocked by browser limits │
│ ├─ DNS Lookup Resolve domain name │
│ ├─ Initial Connection TCP handshake │
│ ├─ SSL TLS handshake │
│ ├─ Request Sent Upload request │
│ ├─ Waiting (TTFB) Time to first byte (server processing) │
│ └─ Content Download Receive response │
│ │
│ For GraphQL, "Waiting (TTFB)" is usually the biggest chunk - │
│ that's your query execution time. │
│ │
└─────────────────────────────────────────────────────────────────────┘

cURL for Testing

# Basic GraphQL query
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer token123" \
-d '{"query": "{ movies { title } }"}'

# With verbose output (see all headers)
curl -v -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ movies { title } }"}'

# Time the request
curl -w "@curl-format.txt" -o /dev/null -s \
-X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ movies { title } }"}'

Summary

┌─────────────────────────────────────────────────────────────────────┐
│ HTTP CHEAT SHEET FOR GRAPHQL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ TYPICAL GRAPHQL REQUEST: │
│ POST /graphql HTTP/1.1 │
│ Content-Type: application/json │
│ Authorization: Bearer <token> │
│ {"query": "...", "variables": {...}, "operationName": "..."} │
│ │
│ TYPICAL GRAPHQL RESPONSE: │
│ HTTP/1.1 200 OK │
│ Content-Type: application/json │
│ {"data": {...}, "errors": [...], "extensions": {...}} │
│ │
│ KEY POINTS: │
│ • GraphQL uses POST for everything (GET optional for queries) │
│ • Always returns 200 unless transport-level failure │
│ • Errors are in response body, not status codes │
│ • HTTP caching is hard; use persisted queries or app-level cache │
│ • Subscriptions upgrade to WebSocket │
│ • HTTP/2+ recommended for multiplexed queries │
│ │
│ VERSIONS: │
│ • HTTP/1.1: Works fine, but head-of-line blocking │
│ • HTTP/2: Better, multiplexing over single connection │
│ • HTTP/3: Best, QUIC eliminates TCP-level blocking │
│ │
└─────────────────────────────────────────────────────────────────────┘

HTTP wasn't designed for GraphQL. It was designed for fetching hypertext documents in the early 90s. But here we are, using it to execute complex query languages, stream real-time updates, and build the modern web. That's the beauty of good protocol design - it bends without breaking.


Tim Berners-Lee invented HTTP to share physics papers. Now it carries 7 billion GraphQL queries per day. I'm pretty sure that counts as scope creep.

Sources