# Answering complex web questions:

> **A client makes a PUT request to** `/users/15` **with the body** `{"name": "Alice"}`**. The server contains a user at** `/users/15` **with** `{"name": "Bob", "email": "`[`bob@test.com`](mailto:bob@test.com)`"}`**. What is the expected state of user 15 after a correctly implemented PUT? Why is this considered idempotent, and what risks does it introduce?**

The email is gone. Vaporized. Lost forever (unless you have backups).

**Why?** Because PUT means **complete replacement**. It's not a "merge" or an "update these specific fields" operation. It's "throw away everything at this address and put this new thing in its place."

**Why Is This Considered Idempotent?**

**Simple Definition:** An operation is idempotent if doing it once has the exact same result as doing it ten times in a row.

***With PUT:***

*   **First request:** Replace drawer contents with `{"name": "Alice"}`. Result: Drawer contains `{"name": "Alice"}`.
    
*   **Second identical request:** Replace drawer contents with `{"name": "Alice"}`. Result: Drawer *still* contains `{"name": "Alice"}`.
    
*   **Tenth identical request:** Same thing. The world hasn't changed after the first one.
    

> 2\. Explain a real-world scenario where using a POST request instead of a PUT for an update operation could cause a significant, costly bug in an e-commerce system.

When a POST is used instead of a PUT for an update like a shipping address change, a network glitch causing the client to retry the request can lead to cascading side effects. Since POST is non-idempotent, each retry is treated as a fresh operation. This could trigger duplicate label prints in the warehouse, redundant fraud alerts freezing the order, multiple payment authorizations on the customer's card, and unnecessary customer service calls—all adding direct financial costs and degrading customer trust. PUT would have prevented this because its idempotent contract ensures that repeated identical requests produce no additional side effects, making it safe for both automatic and manual retries.

> 3\. You have a REST API endpoint `/orders`. A POST request to this endpoint creates a new order. A colleague suggests using GET with a request body to create orders, arguing it's "just semantics." Deconstruct why this is a terrible idea on both a technical and architectural level.

GET with a body breaks HTTP caching, is ignored by most infrastructure, violates the fundamental safety contract of the web (allowing crawlers and prefetchers to trigger destructive writes), and eliminates the self-documenting nature of REST APIs—it's not 'just semantics,' it's sacrificing the only thing that makes the web work at scale.

**detailed:**

**Technical Reasons**

1.  GET Requests Cannot Have a Body (Per HTTP Specification)
    

The HTTP/1.1 specification (RFC 7231) explicitly states that a GET request payload has no defined semantics. In plain English: the spec doesn't forbid sending a body with GET, but it says the server must ignore it. If your server actually reads it, you are violating the protocol.

2.  Most Infrastructure Will Strip or Refuse the Body
    

Many proxies, load balancers, CDNs, and HTTP libraries simply strip the body from GET requests or throw an error. Your "smart" GET-with-body might work on your laptop during development, but it will silently break in production when it hits Nginx, AWS API Gateway, Cloudflare, or a standard fetch() polyfill that discards non-standard payloads.

3.  Caching Servers Will Cache the Wrong Thing
    

GET responses are cached by default based solely on the URL. Two different order creation attempts with the same URL /orders but different bodies would be seen as identical by a cache. The first response could be served from cache for every subsequent request, even if the bodies are completely different orders.

**Architectural Reasons 4. Violation of the Single Most Fundamental Web Contract**

The entire internet operates on an unspoken promise: GET is safe and read-only. Search engine crawlers, browser prefetchers, browser extensions, and monitoring bots all feel free to issue GET requests to any URL they discover. If your GET /orders endpoint actually creates a new order, a Googlebot crawling your site could create thousands of phantom orders just by following links. A browser prefetcher hovering over a link could trigger a purchase.

5.  Browser Behavior Will Work Against You
    

Browsers are allowed to prefetch GET requests aggressively for performance. They also retry failed GET requests automatically without warning. If a user hits "Back" on a page that made a GET to /orders, the browser might replay it from cache without hitting your server at all—or worse, replay a stale version. This is why browsers show the "Confirm Form Resubmission" warning on POST, but never on GET.

6.  Logging and Monitoring Become Meaningless
    

Every monitoring tool in the industry treats GET as a read and POST/PUT/DELETE as a write. A spike in GET requests is considered harmless. A spike in POST requests triggers alerts. By burying destructive operations inside GET, you blind your own operations team.

7.  It Destroys API Discoverability
    

A new developer looking at the API sees GET /orders and naturally assumes it fetches a list of orders. There is no way to know it creates one. RESTful conventions exist so humans don't have to read documentation for every single endpoint. You've just created a landmine that will blow up in someone's face.

> 4\. The PATCH method is described as used for “partial updates.” Describe a specific JSON patch format (like RFC 6902) and explain how it handles a complex operation such as “remove the third item from a list and replace it with a new object,” and why a simple key-value merge would fail here.

**The Starting Resource**

Imagine an online course platform. The endpoint `/courses/python-101` returns this JSON:

```plaintext
{
  "title": "Python for Beginners",
  "instructor": "Alice",
  "modules": [
    {"name": "Intro", "duration": "10min"},
    {"name": "Variables", "duration": "15min"},
    {"name": "Loops", "duration": "20min"},
    {"name": "Functions", "duration": "25min"}
  ]
}
```

**The Goal:** Remove the third module ("Loops") and put a new module in its exact position:

```plaintext
{"name": "Intermediate Topics", "duration": "45min"}
```

**Why a Simple Key-Value Merge Fails**

A simple merge PATCH (like `application/merge-patch+json`) would look like this:

```plaintext
PATCH /courses/python-101 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "modules": [
    null,
    null,
    {"name": "Intermediate Topics", "duration": "45min"}
  ]
}
```

**The intention:** "Replace the third item, leave others alone."

**The actual result:** The entire `modules` array is replaced with exactly what was sent. The server sees the `modules` key and does a full replacement:

json

```plaintext
{
  "title": "Python for Beginners",
  "instructor": "Alice",
  "modules": [
    null,
    null,
    {"name": "Intermediate Topics", "duration": "45min"}
  ]
}
```

The first two modules became `null`, and the fourth module ("Functions") vanished entirely. You didn't "modify one element," you accidentally nuked the whole array.

**Why this happens:** Simple key-value merges treat arrays as atomic values. You cannot express "the third element" or "remove an element at an index." Arrays are replaced wholesale or not at all.

**How <mark class="bg-yellow-200 dark:bg-yellow-500/30">RFC 6902 (JSON Patch) </mark> Solves This**

RFC 6902 defines a completely different format. Instead of sending the new state, you send a **sequence of operations.** Each operation specifies:

*   `op`: The operation type (add, remove, replace, move, copy, test)
    
*   `path`: A JSON Pointer to the exact location to modify
    
*   `value`: The new data (when applicable)
    

**For our scenario — "remove the third item and replace it":**

http

```plaintext
PATCH /courses/python-101 HTTP/1.1
Content-Type: application/json-patch+json

[
  {
    "op": "remove",
    "path": "/modules/2"
  },
  {
    "op": "add",
    "path": "/modules/2",
    "value": {"name": "Intermediate Topics", "duration": "45min"}
  }
]
```

**Step-by-Step Execution**

The server applies these operations in order, like following a recipe:

**Operation 1 — Remove:**

```plaintext
{
  "op": "remove",
  "path": "/modules/2"
}
```

*   `/modules/2` uses zero-based indexing. Index `2` is the third element ("Loops").
    
*   The server deletes it. The array shifts left automatically.
    
*   Resulting array: `["Intro", "Variables", "Functions"]`
    

**Operation 2 — Add at the same index:**

```plaintext
{
  "op": "add",
  "path": "/modules/2",
  "value": {"name": "Intermediate Topics", "duration": "45min"}
}
```

*   The server inserts the new module at index `2`.
    
*   Since "Functions" was at index 2 after the remove, it gets pushed to index 3.
    
*   Resulting array: `["Intro", "Variables", "Intermediate Topics", "Functions"]`
    

**Final Resource State:**

```plaintext
{
  "title": "Python for Beginners",
  "instructor": "Alice",
  "modules": [
    {"name": "Intro", "duration": "10min"},
    {"name": "Variables", "duration": "15min"},
    {"name": "Intermediate Topics", "duration": "45min"},
    {"name": "Functions", "duration": "25min"}
  ]
}
```

Exactly what we wanted. The untouched parts of the document remained perfectly intact.

**Other Powerful Operations in RFC 6902**

The same format handles even more complex manipulations that are impossible with simple merges:

| **Operation** | **What It Does** | **Example** |
| --- | --- | --- |
| `replace` | Overwrite a value at a path | `{"op": "replace", "path": "/instructor", "value": "Bob"}` |
| `move` | Move a value from one path to another | `{"op": "move", "from": "/modules/0", "path": "/modules/3"}` |
| `copy` | Duplicate a value | `{"op": "copy", "from": "/modules/0", "path": "/modules/4"}` |
| `test` | Assert a value is as expected (for optimistic locking) | `{"op": "test", "path": "/title", "value": "Python for Beginners"}` |

**The Key Insight**

A simple key-value merge treats the JSON as a flat dictionary. JSON Patch treats it as a **tree of addressable nodes.** The path `/modules/2/name` uniquely pinpoints a single leaf in the structure, regardless of nesting depth.

This is the difference between saying:

*   "Here's the new modules array, figure it out" (merge patch — destructive)
    
*   "Execute this precise surgical operation at exactly this location" (JSON patch — safe)
    

"RFC 6902 JSON Patch uses an array of operations (add, remove, replace, move, copy, test) with JSON Pointer paths to precisely target locations in a document. To replace the third item in an array, you'd issue a `remove` at `/modules/2` followed by an `add` at the same path. A simple key-value merge fails because it treats arrays as atomic units—you can only replace the entire array, losing all other elements, not surgically manipulate individual indices."

> 5\. A developer notices that making the exact same POST request twice to `/api/payments` results in two separate charges on a customer's credit card. Analyze this situation. Which HTTP method's lack of property caused this, and how should the system be redesigned using a different approach (think idempotency keys)?

### **The Situation Analyzed**

A customer clicks "Pay Now" once. Their browser sends this:

http

```plaintext
POST /api/payments HTTP/1.1
Content-Type: application/json

{
  "order_id": "ORD-9876",
  "amount": 99.99,
  "card_token": "tok_visa_4242"
}
```

Which HTTP Method's Property Caused This?

**POST lacks idempotency.**

That is the single root cause. Idempotency means: "Making the same request one time or ten times produces the same outcome." POST makes no such guarantee. Each POST is treated as a new, independent instruction by default. The server has no built-in mechanism to recognize "this is the same payment, please don't charge twice."

### **Why This Is Fundamentally a POST Problem**

| **Method** | **Idempotent?** | **What Retry Means** |
| --- | --- | --- |
| GET | Yes | "Fetch the same resource again" — harmless |
| PUT | Yes | "Set the resource to this state again" — same state, no extra side effect |
| DELETE | Yes | "Delete this resource again" — already gone, no extra effect |
| **POST** | **No** | **"Do this action again" — charge the card again, create another order, send another email** |

POST is designed for non-repeatable actions. The problem is that network failures make retries inevitable. So you need a layer on top of POST to safely handle duplicates.

**The Solution: Idempotency Keys**

An **idempotency key** is a unique identifier generated by the client for each distinct operation. The server stores the result of processing that key and never processes it twice.

### **How It Works Step by Step**

**Step 1: The Client Generates a Unique Key**

Before making the payment request, the client creates a unique key. This is typically a UUID:

```plaintext
idempotency-key: 4f7b2a1c-8e3d-4a9f-b6c5-1d2e3f4a5b6c
```

The client stores this key alongside the user's checkout session. If the user clicks "Pay Now" again due to a timeout, the client sends the **same key** with the retry.

**Step 2: The First Request Arrives**

```plaintext
POST /api/payments HTTP/1.1
Content-Type: application/json
Idempotency-Key: 4f7b2a1c-8e3d-4a9f-b6c5-1d2e3f4a5b6c

{
  "order_id": "ORD-9876",
  "amount": 99.99,
  "card_token": "tok_visa_4242"
}
```

The server receives this request. It does two things atomically (in a single database transaction or using a distributed lock):

1.  Checks a dedicated `idempotency_keys` table to see if the key `4f7b2a1c-...` has been seen before.
    
2.  It hasn't. So the server:
    
    *   Processes the payment (charges Stripe/PayPal).
        
    *   Stores the result in the `idempotency_keys` table:
        

Step 3: The Retry Arrives (Same Key)

The user clicks "Pay Now" again. The client sends the exact same request with the same idempotency key.

```plaintext
POST /api/payments HTTP/1.1
Idempotency-Key: 4f7b2a1c-8e3d-4a9f-b6c5-1d2e3f4a5b6c

{
  "order_id": "ORD-9876",
  "amount": 99.99,
  "card_token": "tok_visa_4242"
}
```

The server checks the `idempotency_keys` table. The key exists. It does **not** charge the card again. It simply pulls the previously stored response and returns:

What Happens If the First Request Was Still Processing? A common edge case: the first request arrives, starts processing (payment gateways can take seconds), and before it finishes, the retry arrives.

Without proper locking: Both requests run simultaneously. Double charge.

With proper idempotency key implementation: The server must use an atomic "lock and check" mechanism.

Simple Database Approach:

```plaintext
-- Attempt to insert the key. The unique constraint acts as a lock.
INSERT INTO idempotency_keys (idempotency_key, status) 
VALUES ('4f7b2a1c-...', 'processing');

-- If the insert fails because the key already exists:
--   - If status is 'complete', return the stored response.
--   - If status is 'processing', wait or return 409 Conflict telling client to retry later.
```

The unique constraint on `idempotency_key` ensures only one process can claim it. The second request sees the lock and either waits or gets told "still working."

**Key Design Decisions for a Production System**

1.  Key Expiry: Don't store idempotency keys forever. Payment keys can expire after 24 hours. After that, a retry with the same key should be treated as a brand new request (the original is assumed to have failed permanently).
    
2.  Scope: The idempotency key should be scoped per merchant account or per API key. The same UUID from two different customers should not collide.
    
3.  Request Fingerprinting (Optional Extra Safety): Some APIs store a hash of the request body alongside the key. If a retry has the same key but a different body (e.g., a different amount), the server returns a 422 Unprocessable Entity error. This catches client-side bugs where a key is accidentally reused for a different transaction.
    

### The One-Line Interview Answer

"POST lacks idempotency, so network retries cause duplicate side effects like double charges. The solution is an idempotency key—a client-generated UUID sent in the Idempotency-Key header. The server stores the key and its response atomically; if the same key arrives again, the server returns the stored result without re-executing the operation, guaranteeing exactly-once processing semantics atop the inherently non-idempotent POST method."

> 6\. In a microservices architecture, Service A needs to tell Service B to delete a user, but Service A must not wait for Service B to finish the potentially long cleanup job. Would a standard DELETE request be appropriate? If not, propose an alternative HTTP method and a status code strategy to achieve this fire-and-forget behavior.

**The Problem with Standard DELETE**

A standard DELETE request is synchronous by default. Here's what happens:

```plaintext
DELETE /users/789 HTTP/1.1
Host: service-b.internal
```

Service B receives this request and begins its cleanup job:

*   Delete user from the primary database.
    
*   Delete user's uploaded files from object storage (could take seconds for many large files).
    
*   Send deletion events to a message queue.
    
*   Trigger cascading deletes in 15 other microservices (posts, comments, analytics, recommendations, billing).
    

Service A's HTTP connection stays open, waiting. If this cleanup takes 30 seconds, Service A is blocked for 30 seconds. If Service B's container hits a timeout, Service A gets a `504 Gateway Timeout`. Service A has no idea if the delete succeeded or failed.

This defeats the purpose of microservices: Service A is now coupled to Service B's latency and availability.

**Is DELETE Appropriate?**

**No, not for this use case.**

DELETE implies the client wants to know the outcome. The HTTP specification expects a response indicating success (200/204) or failure. A synchronous DELETE is the wrong tool for a fire-and-forget operation.

But here's the nuance: you should still use DELETE. The fix isn't changing the HTTP method. The fix is changing the **interaction pattern** around it.

### **The Correct Pattern: 202 Accepted (Asynchronous Delete)**

*Use DELETE, but with an immediate acknowledgment followed by asynchronous processing.*

**Step 1: Service A Sends the DELETE Request**

```plaintext
DELETE /users/789 HTTP/1.1
Host: service-b.internal
```

**Step 2: Service B Immediately Responds with 202 Accepted**

```plaintext
HTTP/1.1 202 Accepted
Location: /users/789/deletion-status
Content-Type: application/json

{
  "status": "pending",
  "message": "Deletion request accepted. Processing has begun."
}
```

Key points:

*   `202 Accepted` means "I received your request, I understand what you want, but I haven't finished processing it yet." This is exactly the semantics you need.
    
*   The `Location` header provides a URL where the deletion status can be checked later if needed.
    
*   The connection closes immediately. Service A is unblocked. Total latency: milliseconds.
    

**Step 3: Service B Processes in the Background**

Service B spawns a background worker, pushes a message to a queue, or continues processing asynchronously after closing the HTTP connection. Service A doesn't care.

### **Why This Is Still DELETE (and Not POST)**

Some developers argue, "If it's asynchronous, use POST to create a 'deletion job' resource."

```plaintext
POST /users/789/deletion-requests  ← Some do this
```

This is overcomplicating the semantics. A deletion request is still a DELETE. The method describes the **intent** (delete this resource), not the **execution model** (synchronous vs. asynchronous). The status code communicates the execution model.

Using DELETE + 202 Accepted is cleaner because:

*   The verb clearly states intent.
    
*   REST clients, monitoring tools, and API documentation still see a DELETE operation.
    
*   You avoid inventing "action" endpoints that drift away from resource-oriented design.
    

### **Different Strategies for Checking Status**

Depending on the use case, you can provide status visibility in several ways:

**Strategy A: Polling with the Location Header (For When Service A Cares Eventually)**

Service B returns a status URL. Service A can optionally check it:

```plaintext
GET /users/789/deletion-status HTTP/1.1
```

Response while still processing:

```plaintext
HTTP/1.1 200 OK

{
  "status": "in_progress",
  "progress": "Deleted 8 of 15 related services"
}
```

Response once complete:

```plaintext
HTTP/1.1 200 OK

{
  "status": "completed",
  "completed_at": "2026-05-01T10:15:30Z"
}
```

Response if something failed:

```plaintext
HTTP/1.1 200 OK

{
  "status": "failed",
  "error": "Failed to delete billing records: service timed out",
  "failed_at": "2026-05-01T10:16:00Z"
}
```

**Strategy B: Webhook Callbacks (True Fire-and-Forget)**

Service A doesn't even want to poll. It says, "Just tell me when you're done."

```plaintext
DELETE /users/789?callback_url=https://service-a.internal/webhooks/deletion-complete HTTP/1.1
```

Or better, Service A registers its callback URL in a central configuration, and Service B knows it from service discovery. When the deletion finishes (or fails), Service B sends a POST to Service A's callback endpoint.

**Strategy C: Event-Driven (The Most Decoupled)**

Service B doesn't respond with a status URL at all. It accepts the DELETE, returns 202, drops an event onto a message bus (like Kafka or RabbitMQ), and closes the connection.

Service A (and any other interested service) subscribes to `user.deleted` events. When Service B finishes, it publishes the event. Service A receives it asynchronously.

```plaintext
HTTP/1.1 202 Accepted
# That's it. No Location header. Everything is event-driven.
```

* * *

### **Comparison of Alternatives**

| **Approach** | **Method** | **Status Code** | **Coupling** | **Best For** |
| --- | --- | --- | --- | --- |
| Synchronous Delete | DELETE | 200/204 | High | Fast, simple deletions |
| Async with Polling | DELETE | 202 + Location header | Low-Medium | When Service A needs confirmation eventually |
| Async with Webhook | DELETE | 202 | Low | True fire-and-forget with completion notification |
| Event-Driven | DELETE | 202 | Very Low | Fully decoupled microservices |

### **What Happens If Service B Crashes Mid-Delete?**

This is the real engineering concern. The 202 Accepted pattern doesn't solve distributed failures magically. You need additional safeguards:

*   **Idempotency on retry:** Service A can safely retry the DELETE with the same idempotency key. If Service B already processed it, it returns the stored response.
    
*   **Eventual consistency:** The `Location` URL should return `in_progress` indefinitely if the process crashed, allowing a monitoring system to alert on stale deletions.
    
*   **Saga pattern:** For complex multi-service deletions, use compensating transactions if something fails mid-way.
    

### Here's a minimal .NET example demonstrating the asynchronous DELETE with 202 Accepted pattern.

**Service B: The API That Handles Deletion**

```plaintext
// DeletionController.cs
[ApiController]
[Route("users")]
public class UserDeletionController : ControllerBase
{
    private readonly IBackgroundTaskQueue _queue;

    public UserDeletionController(IBackgroundTaskQueue queue)
    {
        _queue = queue;
    }

    [HttpDelete("{userId}")]
    public IActionResult DeleteUser(int userId)
    {
        // Generate a tracking ID for status polling
        var trackingId = Guid.NewGuid();

        // Enqueue the actual cleanup work to run in background
        _queue.Enqueue(async (cancellationToken) =>
        {
            // Long-running cleanup: delete files, notify services, etc.
            await Task.Delay(5000); // Simulating 5 seconds of work
            DeletionStore.MarkComplete(trackingId);
        });


        // Immediately return 202 with a status URL
        var statusUrl = Url.Action(nameof(DeletionStatus), new { trackingId });

        return Accepted(statusUrl, new
        {
            status = "pending",
            message = "Deletion accepted and being processed."
        });
    }

    [HttpGet("deletion-status/{trackingId}")]
    public IActionResult DeletionStatus(Guid trackingId)
    {
        var status = DeletionStore.GetStatus(trackingId);

        return status switch
        {
            null => NotFound(),
            "completed" => Ok(new { status = "completed" }),
            _ => Ok(new { status = "in_progress" })
        };
    }
}
```

* * *

**Simple In-Memory Status Store (For Demo Purposes)**

```plaintext
// DeletionStore.cs
public static class DeletionStore
{
    private static readonly ConcurrentDictionary<Guid, string> _statuses = new();

    public static void MarkPending(Guid trackingId)
    {
        _statuses[trackingId] = "pending";
    }

    public static void MarkComplete(Guid trackingId)
    {
        _statuses[trackingId] = "completed";
    }

    public static string? GetStatus(Guid trackingId)
    {
        return _statuses.TryGetValue(trackingId, out var status) ? status : null;
    }
}
```

* * *

**Service A: The Calling Service (Fire and Forget)**

```plaintext
// UserCleanupService.cs
public class UserCleanupService
{
    private readonly HttpClient _httpClient;

    public UserCleanupService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task DeleteUserAsync(int userId)
    {
        var response = await _httpClient.DeleteAsync($"users/{userId}");

        response.EnsureSuccessStatusCode();

        // 202 Accepted received. We're done here.
        // Don't wait for the actual deletion to complete.
    }
}
```

* * *

### **Key Points in the Code**

1.  **The DELETE endpoint returns immediately** with `Accepted()` (HTTP 202). No `await` on the cleanup task.
    
2.  **The actual work is pushed to a background queue** and executes independently of the HTTP request lifecycle.
    
3.  **A tracking ID links the initial request to a status endpoint** so Service A can optionally poll later.
    
4.  **Service A calls DELETE and moves on** — it doesn't process the response body or poll the status unless it needs to.
    

This is the cleanest separation: the HTTP contract remains a proper RESTful DELETE, but the execution model is entirely asynchronous.

### **The Interview Answer Summary**

"A standard synchronous DELETE is inappropriate for a long-running fire-and-forget cleanup because it blocks the caller and couples their availability to the operation's latency. The correct approach uses DELETE with a `202 Accepted` response, which immediately acknowledges receipt of the deletion intent while processing continues asynchronously. An optional `Location` header provides a status endpoint for polling, or the services can communicate completion via webhooks or events. This preserves the semantic clarity of DELETE while decoupling the execution timeline."

> 7\. When would you specifically use a 204 No Content response versus a 200 OK response with an empty JSON body `{}` after a successful DELETE operation? What are the semantic and practical differences for a client parsing the response?  

Use `204 No Content` when the absence of a body is semantically meaningful—nothing more needs to be said, as with a successful DELETE. Use `200 OK` with `{}` when maintaining a uniform API contract where clients always expect a parseable JSON structure. The practical difference is that 204 requires clients to check the status code before parsing to avoid a JSON parse error, while 200 with `{}` allows unconditional parsing

> 8\. The HEAD method returns no body. If a server has a misconfiguration and accidentally returns a Content-Length header of 5000 but an empty body, is this a violation of the HTTP specification? Explain why or why not.

The exact specification language matters here. RFC 7231 uses two keywords:

*   **MUST NOT send a body.** This is the absolute rule.
    
*   **SHOULD send the same headers, but MAY omit payload headers.**
    

"MAY omit" is not the same as "MUST omit." A server is *allowed* to include `Content-Length: 5000` in a HEAD response, but only if that is factually the content length of the resource (i.e., what a GET would return). This is actually useful—it tells the client the size of the resource without downloading it.

**The violation is not including** `Content-Length`**. The violation is including a** `Content-Length` **that does not match the actual body size (zero).**

A `Content-Length: 5000` header alongside a zero-byte body creates an inconsistent message. The HTTP specification, in RFC 7230 Section 3.3.3, requires consistency between the declared length and the actual body. If the server declares 5000 bytes, it must deliver 5000 bytes. Since a HEAD response can never deliver a body, it cannot deliver 5000 bytes. Therefore, `Content-Length: 5000` with zero bytes of body is a malformed message.

### **The One-Line Interview Answer**

"Yes, this is a violation. The HTTP specification mandates that a HEAD response must not send a message body. While including a `Content-Length` header is allowed to inform the client of the resource's size, its value must be consistent with the actual body. A `Content-Length: 5000` alongside a zero-byte body creates a self-contradictory message where the declared length doesn't match the delivered content. The correct implementations are either omitting `Content-Length` entirely or setting it to `0`."

> 9\. A frontend developer is debugging why a complex API call is failing due to CORS. They see two requests in the Network tab: one OPTIONS and one POST. The POST is never actually sent. Explain what the browser’s console would likely show as the error if the server’s OPTIONS response included `Access-Control-Allow-Methods: GET, PUT` but not `POST`.

### **The Network Tab Timeline**

The developer sees this sequence:

| **Request** | **Method** | **Status** | **What Happened** |
| --- | --- | --- | --- |
| 1st | OPTIONS | 200 OK | Preflight sent, server responded |
| 2nd | POST | (cancelled) | Never actually sent by the browser |

The POST shows as "cancelled" or appears with a red X, with zero bytes transferred. The browser blocked it before it left the machine.

### **The Console Error Message**

The browser console will show a variation of this error (exact wording depends on browser):

**Chrome:**

```plaintext
Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com' 
has been blocked by CORS policy: Request header field content-type is not allowed by 
Access-Control-Allow-Headers in preflight response.
```

Wait — that's a different error. Let me be precise. The error for missing method permission is:

**Chrome (exact wording):**

```plaintext
Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com' 
has been blocked by CORS policy: Method POST is not allowed by Access-Control-Allow-Methods 
in preflight response.
```

**Firefox:**

```plaintext
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource 
at https://api.example.com/orders. (Reason: Did not find method in CORS header 
‘Access-Control-Allow-Methods’).
```

**Safari:**

```plaintext
Fetch API cannot load https://api.example.com/orders due to access control checks.
Method not allowed by Access-Control-Allow-Methods.
```

### **What the Browser Is Actually Doing**

Here's the step-by-step logic inside the browser's CORS engine:

**Step 1: The JavaScript Code Initiates a POST**

```plaintext
fetch('https://api.example.com/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ item: 'laptop' })
});
```

**Step 2: The Browser Determines a Preflight Is Required**

The browser checks: Is this a "simple request" that can skip preflight?

*   Method is POST ✅ (POST is allowed in simple requests)
    
*   Content-Type is `application/json` ❌ (only `text/plain`, `multipart/form-data`, and `application/x-www-form-urlencoded` are considered simple)
    

Because `Content-Type: application/json` is not a "simple" header, the browser triggers a preflight check.

**Step 3: The Browser Sends the OPTIONS Request**

```plaintext
OPTIONS /orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
```

This is the browser asking: "A script from [app.example.com](http://app.example.com) wants to make a POST with a custom Content-Type. Is that allowed?"

**Step 4: The Server Responds**

```plaintext
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT
Access-Control-Allow-Headers: content-type
```

**Step 5: The Browser Runs the CORS Validation Algorithm**

The browser compares the requested method against the allowed methods:

```plaintext
Requested:  POST
Allowed:    GET, PUT

POST is NOT in the list.
```

The validation fails. The browser does not proceed to step 6 (sending the actual POST).

### **The Exact Console Output During Debugging**

If the developer has the Console tab open during the failed request, they'll see:

```plaintext
[Error] Fetch API cannot load https://api.example.com/orders. 
Method POST is not allowed by Access-Control-Allow-Methods in preflight response.
```

If they expand the error in Chrome DevTools, they might see additional details:

```plaintext
Request URL: https://api.example.com/orders
Request Method: POST (preflight blocked)
Status Code: 200 OK (for preflight)
Preflight Request:
  Access-Control-Request-Method: POST
Preflight Response:
  Access-Control-Allow-Methods: GET, PUT   ← POST is missing here
```

### **Why This Is Confusing for Developers**

The developer sees two misleading things:

1.  **The OPTIONS request returned 200 OK.** It didn't fail. A 200 on the preflight doesn't mean the actual request is allowed. It just means the server processed the preflight query. The failure happens silently in the browser's CORS logic.
    
2.  **The POST shows as "cancelled" with no response.** It's not a server error. It's not a network error. It's purely a client-side block. The request never left the browser. This makes server-side logs useless for debugging — the server never saw the POST.
    

### **How to Fix It**

The server must explicitly include `POST` in the allowed methods:

```plaintext
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, POST, DELETE
Access-Control-Allow-Headers: content-type
```

Or, for a quick broad fix during development:

```plaintext
Access-Control-Allow-Methods: *
```

But `*` for methods is not universally supported by all browsers for requests with credentials.

### **The One-Line Interview Answer**

"The browser console will show an error stating that the POST method is not allowed by the `Access-Control-Allow-Methods` header in the preflight response. The browser's CORS engine compares the requested method (POST) against the server's declared allowed methods (GET, PUT), finds no match, and blocks the actual POST from ever being sent. The preflight OPTIONS returning 200 OK is misleading because CORS validation is a separate client-side check that happens after the preflight succeeds."

> 10\. You are designing an API endpoint for a file upload that takes an unpredictable amount of time to process. The client should not wait for processing to complete. Describe a pattern using HTTP methods and the 202 Accepted status code to handle this asynchronously, including how the client would discover the final status.  

### **The Interview Answer Summary**

"The 202 Accepted pattern splits file upload into submission and processing. The client POSTs the file, and the server returns `202 Accepted` with a `Location` header pointing to a status URL. The server processes the file asynchronously in a background worker, updating the status resource as it progresses through states: `accepted`, `processing`, and finally `completed` or `failed`. The client polls the status URL using exponential backoff to avoid overwhelming the server. For very long operations, a webhook callback can replace polling entirely, with the server POSTing the final result directly to a client-provided URL."

## **Section 2: Status Codes (Tricky Scenarios)**

> 11\. A user tries to access their profile page at `/profile` but is not logged in. The server responds with a 404 Not Found to hide the existence of the page from unauthenticated users. Explain why this is a semantically incorrect, albeit common, use of 404 and what the proper status code should be.

**What actually happened:** The resource *does* exist. The server found it. The server knows exactly what it is. The only problem is that the client hasn't proven their identity yet.

The server is telling a deliberate falsehood to the client. It's like a security guard at a private club who, when asked "Is there a VIP lounge upstairs?", replies "No, this building doesn't have an upstairs." It's a lie told for security purposes, but it's still a lie — and it has consequences.

### Consequences of Misusing 404

1.  Search Engines Will De-Index the Page
    

Googlebot tries to crawl /profile, gets a 404, and removes it from the search index. Even for private pages behind login, this matters for SEO of public-facing marketing pages that link to profile features.

2.  Caching Proxies Cache the "Not Found"
    

Intermediate caches (CDNs, ISP proxies) may cache the 404 response. Later, when the user logs in and tries to access /profile, the cached 404 could be served instead of the actual page. The user sees "Not Found" even though they're now authenticated.

### **401 vs 403: The Critical Distinction**

This is where the "security through obscurity" argument usually comes in. People argue: "If I return 401, the attacker now knows the page exists!"

This is when you use 403, not 404.

| **Code** | **Scenario** |
| --- | --- |
| **401 Unauthorized** | "You are not logged in. Please authenticate. The door is locked, but I'll give you the key if you prove who you are." |
| **403 Forbidden** | "You are logged in. I know exactly who you are. But you still can't enter this room. No amount of re-authentication will help." |

### **The One-Line Interview Answer**

"Responding with 404 for a known resource that requires authentication is semantically incorrect because 404 means 'resource not found,' not 'resource found but locked.' The proper response is `401 Unauthorized` with a `WWW-Authenticate` header, which tells the client the resource exists and can be accessed after providing valid credentials. If the goal is to hide the resource's existence from authenticated-but-unauthorized users, use `403 Forbidden`, not 404 — lying with 404 breaks caching, SEO, debugging, and HTTP semantics."

> 12\. A client receives a 403 Forbidden response. What are the two fundamentally different root causes indicated by the article’s “open gates vs. closed gates” analogy, and how does the distinction impact the client’s subsequent action?

### **The Analogy: Open Gates vs. Closed Gates**

Imagine a gated community with a security guard.

| **Scenario** | **Analogy** | **Meaning** |
| --- | --- | --- |
| **401 Unauthorized** | The gate is closed. The guard says: "I don't know who you are. Show me your ID first, then we'll talk." | You haven't proven your identity. |
| **403 Forbidden** | The gate is open. The guard checked your ID and says: "I know exactly who you are. You still can't come in." | You are known. You are denied. |

Now here's the key insight: a 403 can happen for **two completely different reasons** even though the status code is identical.

### **The One-Line Interview Answer**

"A 403 Forbidden indicates the server knows your identity but is denying access for one of two fundamentally different reasons: insufficient permissions (you have a valid identity but lack the required role for this resource) or explicit denial (you are specifically blocked due to IP restrictions, account suspension, or a resource-level lock). The client must parse the response body's error code to differentiate them — insufficient permissions might be resolved by switching to a privileged account, while explicit denial requires resolution through support channels or a network change."

> 13\. You manage an API that undergoes maintenance every Sunday at 3 AM. During this time, the entire API is unreachable. You want to communicate to automated clients that the downtime is temporary and they should retry after a specific duration. Which 5xx status code should you use, and which response header is critical to include for polite clients?

### **The One-Line Interview Answer**

"Use `503 Service Unavailable` because its RFC definition explicitly covers temporary maintenance. The critical header is `Retry-After`, which can specify either an absolute timestamp or a relative number of seconds, telling clients exactly when to retry instead of guessing. Without it, clients either retry aggressively causing a thundering herd upon recovery, or back off indefinitely assuming permanent failure — both outcomes are worse than a single coordinated retry window."

> 14.A search engine crawler requests an old URL `/old-blog-post`. The server has permanently moved this content to a new URL structure `/blog/old-blog-post`. What status code should be returned? Additionally, explain the long-term consequence for the search engine's index if you mistakenly use a 302 Found instead.

**The One-Line Interview Answer:** Return `301 Moved Permanently` with a `Location: /blog/old-blog-post` header. Using `302 Found` instead tells crawlers the redirect is temporary, so they keep the old URL indexed alongside the new one, creating duplicate content penalties, splitting page rank, and wasting crawl budget — the old URL never drops from the index even years later because crawlers keep returning to check if the "temporary" move has ended.

"Crawlers are automated programs from search engines that visit and index web pages. Their role in redirects is to decide whether to update their index to the new URL (301) or keep both URLs alive expecting a return (302) — using the wrong code wastes crawl budget, splits search ranking, and creates permanent duplicate content problems."

* * *

> A load balancer sits in front of three application servers. A client sends a request, and the load balancer forwards it to Server A, which has crashed. The load balancer cannot establish a connection to Server A. Which 5xx status code should the load balancer itself return to the client?

**The One-Line Interview Answer:** Return `502 Bad Gateway` because the load balancer, acting as a gateway, received a valid client request but received no valid response from the upstream server it depends on — this is not the load balancer's own error (500) nor a timeout waiting for a slow server (504), but a complete failure to get any response from the upstream.

* * *

> A client sends a GET request with an `If-None-Match` header containing an ETag. The server determines the resource has not changed. What specific 3xx status code is returned? Explain the client-side performance benefit of this "conditional request" compared to simply ignoring it.

**The One-Line Interview Answer:** The server returns `304 Not Modified` with no body, allowing the client to reuse its cached copy immediately. The performance benefit is avoiding re-downloading, decompressing, and re-parsing the unchanged resource body — for a page with 50 assets where 45 haven't changed, only 5 updated files transfer over the wire, dramatically reducing bandwidth costs and improving perceived load speed.

* * *

> An API has a rate limit of 100 requests per minute per user. Client A sends the 101st request within the minute. What is the correct status code to return? In the response, the server includes a `Retry-After` header set to `45`. Interpret this instruction for the client.

**The One-Line Interview Answer:** Return `429 Too Many Requests`. The `Retry-After: 45` means the client's quota is exhausted and it must pause all requests to this API for exactly 45 seconds before retrying — not retry immediately, not poll every second, but set an internal timer and wait until the window resets.

* * *

> A payment processing API returns a 400 Bad Request with a plain-text body "Invalid card". The frontend developer complains the error is too generic for good UX. Discuss what the backend developer should include in the 400 response body to make it a "rich" error, using a standard format.

**The One-Line Interview Answer:** Use RFC 7807 Problem Details format to return structured JSON with a machine-readable error code, human-readable detail, and field-level validation errors pointing to exact form fields.

```json
{
  "type": "https://api.example.com/errors/invalid-card",
  "title": "Invalid Card",
  "status": 400,
  "detail": "The provided card information could not be validated.",
  "errors": [
    {
      "field": "card_number",
      "code": "invalid_format",
      "message": "Card number must be 16 digits."
    },
    {
      "field": "expiry_month",
      "code": "expired",
      "message": "The card has expired."
    }
  ]
}
```

This lets the frontend highlight specific failing fields and take different actions based on the `code` rather than parsing free-text strings.

* * *

> A client POSTs data to an endpoint that processes the request and immediately needs to redirect the client's browser to view the newly created resource. Which 3xx status code should be used, and what critical header must accompany it? Which method does the browser use for the subsequent redirect request?

**The One-Line Interview Answer:** Use `303 See Other` with a `Location: /orders/98765` header. The browser will follow the redirect using a GET request — this is critical because `302 Found` might cause some browsers to retry the POST and create a duplicate resource, while 303 explicitly guarantees a GET for the redirect, forming the Post/Redirect/Get pattern that prevents "Confirm Form Resubmission" popups on refresh.

* * *

> A server crashes midway through generating a large CSV export. The connection is severed without a proper HTTP response. The client sees a "Connection Reset" error. How does this "non-response" differ fundamentally from a 500 Internal Server Error, and why is it more difficult for client-side error handling?

**The One-Line Interview Answer:** A `500 Internal Server Error` is a proper HTTP response with a status line, headers, and possibly a body — the client gets a definitive answer even if it's failure. A connection reset (TCP RST packet) is not a response at all — the server vanished mid-sentence with no status code to branch on, no error body to parse, and no way to know if processing partially executed. The client must decide whether to retry blindly, risking duplicate work, <mark class="bg-yellow-200 dark:bg-yellow-500/30"> which is why long-running operations should use </mark> `202 Accepted` <mark class="bg-yellow-200 dark:bg-yellow-500/30"> with status polling instead of synchronous generation.</mark>

> Explain the "HTTPS is HTTP wrapped in TLS" model. If an eavesdropper is on a public Wi-Fi network and you visit an HTTPS site, which parts of the HTTP request (method, path, headers, body) are visible to the eavesdropper during and after the TLS handshake?

**The One-Line Interview Answer:** HTTPS places the entire HTTP message inside a TLS-encrypted tunnel — the method (`GET`), path (`/about`), headers (`Cookie`, `Authorization`), and body are all encrypted and completely invisible to an eavesdropper both during and after the handshake. The only things visible in plaintext are the TLS handshake metadata: the SNI (Server Name Indication revealing the hostname like `example.com`), the server's certificate (which is public anyway), and the negotiated cipher suite — everything after the handshake is scrambled ciphertext indistinguishable from random bytes.

* * *

> In the TLS handshake, the browser uses asymmetric encryption (the server's public key) to securely send the symmetric session key. Why not just use asymmetric encryption for the entire conversation? Why switch to symmetric encryption for the bulk data transfer?

**The One-Line Interview Answer:** Asymmetric encryption is computationally expensive — RSA or ECDH operations are hundreds to thousands of times slower than symmetric ciphers like AES, so using asymmetric encryption for every packet of a multi-megabyte webpage would cripple performance. The handshake uses asymmetric encryption only for the brief task of securely exchanging a shared secret (the session key), then both sides switch to fast symmetric encryption for the actual data transfer, achieving the same security with dramatically better throughput.

* * *

> The article describes Certificate Authorities as a "chain of trust." If a root CA's private key is compromised, every certificate it has ever signed is potentially fraudulent. How do browsers and operating systems mitigate this catastrophic "break glass" scenario?

**The One-Line Interview Answer:** Browsers and OS vendors can push an emergency update that revokes the compromised root CA certificate from their trusted store entirely — removing it from every browser and device that installs the update, which instantly invalidates all certificates chained to that root. Additionally, Certificate Transparency logs provide a public, tamper-proof record of every certificate ever issued, allowing domain owners to audit for fraudulent certificates issued by the compromised CA and trigger the revocation process via OCSP stapling or CRLs before the root is fully distrusted.

> You self-sign a certificate for your local development server. Your browser shows a stark "Your connection is not private" warning and makes you click "Advanced" to proceed. Explain precisely which pillar of TLS (Encryption, Authentication, or Integrity) is compromised in this scenario, and why the connection isn't just downgraded to HTTP.

**The One-Line Interview Answer:** **Authentication** is the compromised pillar — the self-signed certificate provides no verifiable proof that `localhost` is actually `localhost` because no trusted third party (CA) vouched for it, so a man-in-the-middle could present an identical self-signed certificate and impersonate the server. Encryption and Integrity remain fully functional (the connection still uses AES-GCM or similar, and data cannot be tampered with without detection). The browser refuses to downgrade to HTTP because that would silently strip all security; instead it shows the warning as an explicit user choice — "You can proceed, but you are accepting the risk of an unverified identity."

* * *

> A man-in-the-middle attacker intercepts an HTTPS connection. They cannot break the encryption, so they simply capture the encrypted traffic and try to replay the exact same encrypted request a hundred times, hoping one will cause a "Transfer $100" operation to execute a hundred times. How does the TLS record layer's integrity and sequence-number mechanism prevent this replay attack?

**The One-Line Interview Answer:** Every TLS record contains an embedded, incrementing sequence number that is included in the Message Authentication Code (MAC) computation. When the server receives a record, it verifies the MAC against the expected sequence number — if an attacker replays an old record, the sequence number won't match what the server expects (it already processed that sequence once), the MAC verification fails, and the connection is terminated. Replaying the same encrypted bytes is useless because the integrity check binds the ciphertext to a specific position in the stream that cannot repeat.

* * *

> Describe a situation where a web page loaded over HTTPS is perfectly secure, but a resource on that page (like a JavaScript file) is loaded over plain HTTP. What browser security mechanism is triggered, and what is this mixed-content vulnerability called? Which is more dangerous: an HTTP script or an HTTP image, and why?

**The One-Line Interview Answer:** This triggers **Mixed Content Blocking** — browsers block or warn when an HTTPS page includes resources loaded over HTTP. It's called **active mixed content** (scripts, stylesheets, iframes) vs. **passive mixed content** (images, audio, video). An HTTP script is far more dangerous than an HTTP image because an attacker can tamper with the script in transit to execute arbitrary code in the context of the otherwise-secure page, stealing cookies, capturing keystrokes (including passwords), or rewriting the DOM — a compromised image can only be replaced with another image, which is a phishing risk but lacks code execution capability.

* * *

> HTTP Strict Transport Security (HSTS) is a security policy. A server sends the header `Strict-Transport-Security: max-age=31536000; includeSubDomains` on an HTTPS connection. Explain how this header protects a user who is tricked into typing `http://bank.com` directly into their browser on the next visit.

**The One-Line Interview Answer:** Once the browser has visited the site over HTTPS and received the HSTS header, it stores the policy for 31,536,000 seconds (one year). When the user later types `http://bank.com`, the browser internally rewrites the request to `https://bank.com` before the network request ever leaves the device — it never sends a plaintext HTTP request. Without HSTS, the initial HTTP request would go out in cleartext, allowing an attacker on the network to intercept it with a spoofed response before the server could redirect to HTTPS. The `includeSubDomains` directive extends this protection to every subdomain like `api.bank.com` and `login.bank.com`.

* * *

> An attacker creates a fake Wi-Fi hotspot named "Free Airport Wi-Fi" and performs a man-in-the-middle attack, presenting a fake certificate for `google.com`. Explain the step-by-step checks your browser performs that cause it to reject this connection, even though the fake certificate perfectly says "google.com."

**The One-Line Interview Answer:**

1.  **Domain Match:** The browser checks if the certificate's Common Name or SAN (Subject Alternative Name) matches `google.com` — the fake certificate says `google.com`, so this passes.
    
2.  **Expiry Check:** The browser verifies the certificate is within its validity period — the attacker can generate a valid-dated certificate, so this may pass.
    
3.  **Signature Verification:** The browser extracts the issuer's public key and verifies the digital signature on the certificate — if the attacker self-signed it or used a fake CA not in the browser's trust store, signature verification fails and the browser shows the warning.
    
4.  **Chain of Trust to a Root CA:** The browser walks up the certificate chain to find a trusted root CA in its local store — the attacker's certificate chains to an unknown or untrusted root, so the browser rejects the connection. The certificate "perfectly says google.com" but cannot prove it was issued by a CA the browser trusts, and without a valid chain, the browser declares the identity unverified.
    

* * *

> The article states the session key is "thrown away" at the end of the session. What is this property called in cryptography, and why is it a critical security feature? If a server's private key is stolen a year later, are all past recorded encrypted sessions now readable? Explain why or why not.

**The One-Line Interview Answer:** The property is called **Perfect Forward Secrecy (PFS)** — it ensures that compromising long-term keys (the server's private key) does not compromise past session keys. Past encrypted sessions are **not readable** even if the private key is stolen a year later because the session key was derived using an ephemeral Diffie-Hellman (DHE or ECDHE) key exchange — the server's private key was only used for authentication (signing the handshake), not for encrypting the session key itself. The ephemeral private keys used to derive the session key were generated on-the-fly and destroyed immediately after the handshake, so even with the server's private key, an attacker cannot recover the ephemeral key needed to decrypt recorded traffic.

* * *

> You are writing a Python script that uses the `requests` library to call an HTTPS API, but you want to inspect the exact encrypted bytes leaving your machine for debugging. What tool, external to your script, would you use, and how would you set it up to decrypt the traffic without modifying your Python code?

**The One-Line Interview Answer:** Use **mitmproxy** (or Charles Proxy / Fiddler) — run it locally, which generates its own root CA certificate that you install in your OS trust store so your machine trusts it. Configure the proxy to listen on a port (e.g., 8080), then set environment variables before running the Python script:

```bash
export HTTP_PROXY=http://localhost:8080
export HTTPS_PROXY=http://localhost:8080
export REQUESTS_CA_BUNDLE=/path/to/mitmproxy-ca-cert.pem
python my_script.py
```

The proxy acts as a man-in-the-middle: it decrypts the traffic from your script, displays the plaintext HTTP request and response in its UI, then re-encrypts it to the real server (and vice versa), all without modifying a single line of Python code. The `REQUESTS_CA_BUNDLE` environment variable tells the `requests` library to trust the proxy's certificate for the decrypted connection.
