skip to content

An endpoint passes every in-process test but misbehaves behind a TLS-terminating proxy in production - how would you test for that class of fault?

level: seniorimportance: should knowfreq 52%

answer

  1. the test supplied the facts it asserts
  2. configuration is not in the in-process path
  3. assert what an external client observes
  4. trust the header only from the right peer
  5. one case per connection-derived decision

basics

~20 s

Because an in-process test supplies the connection facts itself, it only confirms your assumption about the deployment. Reproduce over a real listener configured the way production is, with the terminator in front, and assert on what an external client sees.

solid answer

~50 s

The fault class is code that branches on facts about the **connection** - whether it was secure, who the peer was, what a hop in front added - because a dispatcher-fed request has no connection, so the test supplies those facts itself. Whatever the test writes, the assertion confirms; that is why it is green while production is not. To test it, start a real listener configured the way the deployment configures it, put the terminating hop in front, and drive it with a real client, asserting on what that client observes: the scheme in absolute links and redirect targets, cookie attributes, the status. Cover the trust boundary in both directions - the injected header ignored from an untrusted peer, honoured from the trusted hop - and keep the set to a handful of cases.

go deeper

for a junior

Take away the shape of the trap: if a test invents the connection facts it then asserts on, it confirms your assumption rather than the deployment's behaviour.

for a middle

Explain which handler decisions come from the connection rather than the request content, and why those are exactly the ones a dispatcher-fed test cannot check.

for a senior

Reproduce over a listener configured like production with the terminating hop in front, assert on links, redirects and cookie attributes, and cover the untrusted-peer direction as well as the trusted one.

for a principal

Own where deployment-shape assumptions are verified overall - a few wire tests, environment provisioning checks, post-deploy probes - and make sure a topology change forces those to be revisited.

## Why the fast suite was green An in-process test client builds the request, including everything the application later reads about the connection. If the handler asks whether the request arrived securely, or what the client address was, or reads a header a proxy is supposed to add, the answer is whatever the test wrote. The test therefore encodes **your assumption about the deployment** and then asserts that assumption back to you. When production disagrees - the terminator is somewhere else, the trusted peer list is empty, the added header never arrives - the test cannot notice, because the disagreement is with the configuration, and the configuration is not in the path. This is the sharpest version of the general rule: an in-process pass proves things about the application, not about the deployment. ## The symptoms this fault class produces | Symptom | What the application got wrong | |---|---| | Generated absolute links and redirect targets use the wrong scheme or host | It inferred the external address from the local connection instead of the terminating hop | | A cookie marked for secure transport only is never set, or is set when it should not be | It decided the connection was insecure because the terminator is a separate hop | | An infinite redirect loop between the application and the terminator | Each side believes the other should perform the upgrade | | Client addresses in logs and rate limits are all the same value | It read the peer address, which is the proxy, not the client | | A client can spoof an identity by sending a header itself | The header is trusted from any peer, not only the terminating hop | ## How to reproduce it deliberately 1. **Start the real listener** with the configuration the deployment uses - the same trust settings, the same address handling, the same secure-transport setting. Bind port `0` so parallel runs do not collide. 2. **Put the terminating hop in front of it** in the test, or stand in for it precisely: a client that connects to the application the way the hop would, sending the same added headers from the same peer. 3. **Assert on what an external client sees**, not on an internal flag: the scheme and host of an absolute link or redirect target, the attributes on a set cookie, the status. Those are the things users and browsers act on. 4. **Test the trust boundary in both directions.** Send the proxy-added header from an untrusted peer and assert it is ignored; send it from the trusted one and assert it is honoured. A test that only covers the honoured direction turns a spoofing hole into a passing suite. 5. **Test the direct path too.** Something usually can still reach the application without going through the hop. Decide what should happen then - refuse, redirect, or treat as insecure - and assert it. ## Keeping it proportionate These tests are slow and they encode deployment shape, so they rot when the topology changes. That argues for a handful, not a suite: - one case per **decision** the application makes from connection facts, not one per route; - run them on every change to the listener or trust configuration, and on a schedule; - when the topology changes - the terminator moves, a hop is added - treat these tests as the first thing to update, because their whole value is that they encode the shape; - pair them with a post-deploy check against the real environment, since only that sees the real hop. ## Where the assumption is actually stored It is worth being precise about what fails here, because the instinct is to go looking in the handler. The handler is usually fine: given a request that says the connection was secure and came from a particular client, it does the right thing. What is wrong is **the pipeline that produces that statement** - the listener's own security setting, the list of peers whose added headers are trusted, and whether the hop in front is adding them at all. None of that is code you wrote; it is configuration, and configuration only participates when a real listener starts. That is the whole reason the fault class survives a fast suite of any size. ## The habit worth forming When you read a handler, notice every place it consults something about the connection rather than about the request's content. Each of those is a line an in-process test cannot exercise honestly, and each is a candidate for one of the few tests that go over the wire. That short list, rather than a general instinct that integration tests are better, is what decides where the expensive coverage goes.

  • Why is adding the proxy header to an in-process request not enough?
    Because the question is not what the application does with a header it receives, but whether the deployment delivers one and whether the application trusts it from that peer. Both are listener configuration, which an in-process call bypasses, so the test proves only that the handler can read a value you wrote.
  • What should the negative test look like?
    An external client sends the proxy-added header itself, over a connection that did not come from the trusted hop, and the application must ignore it. Without that case, a configuration that trusts the header from anyone passes every test while letting a client claim any address or scheme it likes.
  • Why assert on links and cookie attributes rather than on an internal flag?
    Because the internal flag is the assumption under test. Absolute links, redirect targets and cookie attributes are what a browser actually acts on, so asserting them catches the fault in the form users experience, and survives refactoring of how the application derives the connection facts.

saying these in an interview costs you the question

  • Sets the proxy header in an in-process request and calls it verified
  • Assumes the application behaves the same whether the terminator is separate
  • Trusts a forwarded header regardless of which peer sent it
  • Blames the handler when the listener configuration was never exercised
  • Moves the whole suite over the wire instead of a few pinned cases