# IHttpRequestHandler: What Makes HttpCalloutMock Awkward, and ApexTools' Answer

When you test an external integration in Salesforce, you swap the response through the standard `HttpCalloutMock`. ApexTools' `IHttpRequestHandler` makes the callout **replaceable through DI, and lets you assemble responses by declaration alone**.

> 📌 **The platform does ship built-in mocks.** With `StaticResourceCalloutMock` / `MultiStaticResourceCalloutMock` you can return a response without writing an implementation class. But **the response body has to exist as a static resource (metadata)**, and it is one response per endpoint, so **an ordered sequence of responses (retries, pagination) cannot be expressed**. The moment you need that, you are back to implementing `HttpCalloutMock`.

## What it gives you

- Inject `HttpRequestHandler` (a thin wrapper over the standard `Http`) in production and `MockHttpRequestHandler` in tests
- Declare a response in one line: `MockResponse.of('GET').respond(body, 200)`
- **No `HttpCalloutMock` implementation class, even for ordered responses**
- Inspect the requests you sent, after the fact (Spy)

## Three things that make HttpCalloutMock awkward

These are what you hit once you end up implementing `HttpCalloutMock` yourself.

### 1: Hand-writing JSON strings

The response body ends up as a string literal, costing you both escaping and readability. (You can push it into a static resource, but then reading the test means opening another file.)

```apex
// ❌ falls apart as the structure deepens
String body = '{"records":[{"Id":"001xx","Name":"Acme","Contacts":{"totalSize":2}}]}';
```

### 2: if-else branching on URL strings

One mock class carries every endpoint, so the inside of `respond()` swells with URL checks.

```apex
// ❌ the branching grows with every new endpoint
public HttpResponse respond(HttpRequest req) {
  if (req.getEndpoint().contains('/accounts')) { ... }
  else if (req.getEndpoint().contains('/contacts')) { ... }
  ...
}
```

### 3: Retries and pagination are hard to reproduce

Expressing **an ordered set of responses** — "500 the first time, 500 again, then 200" — means giving the mock a counter of its own.

## ApexTools' answer

### Declare responses with MockResponse

```apex
MockResponse.of('GET').respond('{"message":"not found"}', 404)          // String
MockResponse.of('post').respond(new Map<String, Object>{ ... }, 200)    // Map / List get JSON-encoded. Method name is case-insensitive
MockResponse.of('GET').respond(imageBytes, 200).header('Content-Type', 'image/jpeg')  // Blob + header
MockResponse.of('GET').respond(ok, 200).repeat()                        // last in the queue: every call from here on
```

`respond` accepts `String` / `Map<String, Object>` / `List<Object>` / `Blob`. Maps and lists are JSON-encoded automatically, so **awkwardness 1 dissolves into "write it as an Apex collection"**.

> ⚠️ **The method name is not a routing key. It is the contract at delivery time.** If the next response in the queue declares a method that does not match the actual request, it fails immediately with an error carrying the expectation, the actual, and the queue state. A different response is never handed over silently.

## Two modes

**🎯 Rule of thumb: if the test's claim includes ordering, use script mode; if it does not, use label mode. When in doubt, label.**

### Script mode (no labels): when ordering is the specification

The list you pass to the constructor is the script of the flow. Read it top to bottom and it is the sequence of callouts you expect.

```apex
MockHttpRequestHandler mock = new MockHttpRequestHandler(new List<MockResponse>{
  MockResponse.of('GET').respond(notFound, 404),    // move 1: does it exist?
  MockResponse.of('POST').respond(created, 201),    // move 2: create
  MockResponse.of('GET').respond(found, 200)        // move 3: fetch again
});
```

Deviating from the order or the method produces a detailed error. **Awkwardness 3 becomes "list the same method a few times"**.

```apex
// 500, 500, then success. No counter in the mock
new List<MockResponse>{
  MockResponse.of('POST').respond(err, 500),
  MockResponse.of('POST').respond(err, 500),
  MockResponse.of('POST').respond(ok, 200)
}
```

### Label mode: when you do not want to be tied to cross-site ordering

Give each callout site its own named queue (the same feel as `MockEloquent`'s `attach` / `label`). **Awkwardness 2 dissolves because you sort by the name of the call site, not by inspecting the URL.**

```apex
// In the Usecase: this.http.label(LBL_EXISTS).send(req);
MockHttpRequestHandler mock = new MockHttpRequestHandler()
  .attach(LBL_EXISTS, MockResponse.of('GET').respond(notFound, 404))
  .attach(LBL_CREATE, MockResponse.of('POST').respond(created, 201))
  .attach(LBL_UPDATE, MockResponse.of('PUT').respond(updated, 200));   // declaring an untaken branch is fine

new KintoneUpsertUsecase(input, mock).invoke();

Assert.areEqual(1, mock.sentRequestsAt(LBL_CREATE).size());   // the create branch was taken
Assert.areEqual(0, mock.sentRequestsAt(LBL_UPDATE).size());   // update was never consumed
```

For a branching flow the standard move is to **attach both branches and assert which one got consumed**.

- Once you use `attach`, every `send` must be preceded by `label()` (consumed once per send)
- A typo'd label fails with the list of registered labels attached
- Attaching to the same label again appends to its queue (that site's retry sequence)
- **Without `attach`, `label()` calls are ignored.** Labelled production code can still be tested against a plain script mock

## Verification helpers (Spy)

| Method | Purpose |
|---|---|
| `sentRequestsAt(label)` | The requests sent under that label |
| `countByMethod('POST')` | How many were sent per HTTP method |
| `requestsTo(endpointPart)` | Filter by a substring of the endpoint |
| `lastRequest()` | The last request sent |
| `describe()` | The current queue state (for debugging) |

## 🛡 The Content-Type guard (a habit born from a real incident)

Even on HTTP 200, an unexpected Content-Type usually means **you hit the wrong endpoint**.

> A real case: the code meant to call `/bizCards/{id}/image` but was calling `/bizCards/{id}`, base64-encoding the returned JSON and pushing a corrupted image to the UI.

Always guard when fetching binary.

```apex
this.http.label(LBL_CARD_IMAGE).send(req);
String contentType = this.http.getHeader('Content-Type');
if (this.http.getStatusCode() == 200 && (contentType == null || !contentType.startsWith('image/'))) {
  throw new CalloutException('Expected an image response but got Content-Type=' + contentType);
}
Blob image = this.http.getBodyAsBlob();
```

## Integrating with Apex Stem: injecting into a Usecase

`IHttpRequestHandler` slots straight into the Apex Stem Usecase layer and the [Layered Constructor Pattern](/apex-stem/docs/layered-constructor-pattern). **From v1.0.0 the recommendation is to multiplex a single handler with labels** (the same idea as `IEloquent`'s `label`).

```apex
public with sharing class KintoneUpsertUsecase {
  @TestVisible static final String LBL_EXISTS = 'kintoneExists';
  @TestVisible static final String LBL_CREATE = 'kintoneCreate';

  private final Input input;
  private final IHttpRequestHandler http;
  private Trace t = Trace.of('Upsert a record into kintone');

  // 🚪 production
  public KintoneUpsertUsecase(Input input) {
    this(input, null);
  }

  // 🧪 test (DI)
  @TestVisible
  private KintoneUpsertUsecase(Input input, IHttpRequestHandler http) {
    this.input = input;
    this.http = http ?? new HttpRequestHandler();
  }

  public void invoke() {
    this.t.start();
    this.http.label(LBL_EXISTS).send(existsReq);
    // ...
  }
}
```

Injecting several handlers by role still works, but **multiplexing by label keeps the constructor from bloating**.

Inject `MockEloquent` (ApexEloquent) and `MockHttpRequestHandler` (ApexTools) independently and you can **verify side effects (DML) and outbound calls (HTTP) on separate axes**.

## ⚠️ Breaking changes in v1.0.0

Upgrading from a pre-tag `main` needs three things.

| Change | What to do |
|---|---|
| The old constructors (`Map` / `List<Map>` / `String` + `Integer`) were removed | Rewrite as `MockResponse.of(method).respond(body, statusCode)` |
| `label` / `getBodyAsBlob` / `getHeader` were added to `IHttpRequestHandler` | Add the methods to any custom implementation |
| The exhaustion error message became a multi-line diagnostic | Loosen exact-match asserts to `contains` |

## Related Documents

- [Layered Constructor Pattern](/apex-stem/docs/layered-constructor-pattern): the base pattern behind `IHttpRequestHandler`'s DI design
- [TriggerHandler](/apex-stem/docs/apex-tools-trigger-handler): the other pillar of ApexTools
- [ApexTools guide](/apex-stem/docs/apex-tools-guide): back to the guide index
