skip to content

A CI step calls ZAP's control API with a wrong key and gets no response at all — why, and what would you change?

level: seniorimportance: should knowfreq 40%

answer

  1. not every refusal is an error document
  2. the connection ends rather than answers
  3. the silence is deliberate, and aimed elsewhere
  4. one option makes permission failures speak

basics

~20 s

A bad key is refused silently: the program writes no response and the connection is closed, so there is no status code to read. Set api.reportpermerrors to make permission failures answer 400 with a code and message instead.

solid answer

~40 s

Most failures on this surface answer `400` with a small `{"code", "message"}` document. Two do not: **an invalid or missing API key, and a call refused because the API is disabled, produce no response at all** — the handler returns an empty message and the listener closes the connection. A refusal by the permitted-address list, or by `api.secure` on a plaintext request, behaves the same way. The reasoning is that an unauthenticated stranger should not be told there is a ZAP here. The cost is that a wrong key, a wrong address, a wrong port and a program that has not started all present identically to a pipeline step. Setting `api.reportpermerrors` makes those refusals answer `400 bad_api_key` instead, which is worth doing on a runner nothing else can reach.

code

json · 1 line
json
{"code": "bad_api_key", "message": "Invalid or missing API key"}

go deeper

for a junior

Know that a wrong key here does not produce a normal HTTP error — the call comes back empty. Do not read that as the program being down until you have checked the credential.

for a middle

Explain which refusals are silent and which answer 400 with a machine-readable code, and what api.reportpermerrors changes. Be able to name the other conditions that share the silent path.

for a senior

Diagnose from the shape of the failure rather than its presence: prove liveness separately, read status codes, and make the step fail loudly instead of continuing into a run that never happened.

for a principal

Weigh the disclosure trade-off deliberately. Readable permission errors are cheap on a runner nothing else can reach and a gift to a stranger anywhere else, so the answer should differ by environment rather than be one global default.

## Some refusals are deliberately silent Most failures on ZAP's control API answer politely. Ask for a component that is not registered, omit a mandatory parameter, pass a value of the wrong type, and you get `400 Bad Request` with a small document in the format you asked for: ```json {"code": "illegal_parameter", "message": "Provided parameter has illegal or unrecognized value"} ``` Two failures do not. **An invalid or missing key, and a call refused because the API is disabled, are dropped without any reply at all**: the handler builds no response, and the listener closes the connection. From the caller's side that is not an HTTP error — it is a connection that opened and then ended with nothing on it. `curl` reports an empty reply from server; a client library raises a connection error rather than an HTTP status. Two further refusals behave the same way for the same reason: a request from an address that is not on the permitted list, and a plaintext request when `api.secure` requires HTTPS. Both are decided before the call is ever resolved, and both end the connection in silence. ## Why it is built that way Silence is the correct answer to an unauthenticated stranger. A `401` with a body saying "Invalid or missing API key" is a confirmation that there is a ZAP here and that guessing a credential is the remaining work. Saying nothing gives a stranger sweeping a network nothing that distinguishes this listener from a closed one. The cost lands on the legitimate caller, and specifically on an unattended one. **A pipeline step cannot tell these apart from the failure alone:** - the key is wrong, or was not sent; - the request came from an address the instance does not permit; - the step is pointed at the wrong host or port; - the program has not finished starting, or has already exited. Every one of those presents as "nothing came back". That is the diagnosis problem: the most common configuration mistake on this surface produces the same symptom as the tool not being there. ## Turning the answer back on Two options change what a refusal says. | option | default | effect | |---|---|---| | `api.reportpermerrors` | off | permission failures answer `400` with a `code` and `message` instead of closing the connection | | `api.incerrordetails` | off | error documents carry an extra `detail` field naming the offending parameter or condition | With `api.reportpermerrors` on, a wrong key becomes a readable `bad_api_key` / "Invalid or missing API key" response, and the ambiguity collapses: a 400 means you reached the program and it refused you, while silence now really does mean you never reached it. Both options widen what an unauthenticated caller learns, which is exactly why they are off by default and why they sit under the program's own testing-only warning. The judgement is about exposure, not about convenience: on a runner that only the pipeline can reach, turning them on during bring-up costs little and saves hours; leaving them on for an instance anything else can reach hands a stranger a confirmation and a hint. ## What a step should do instead of guessing A step that drives this surface directly should not treat "no response" as a single condition: 1. **Prove liveness separately from authentication.** A read that needs no state — the version view — establishes that something is answering before any keyed call is attempted. If even that returns nothing, the problem is reachability or the key, not the call you care about. 2. **Check the status code, not just whether a body parsed.** A `400` with `{"code": …}` is a parseable document. A step that only asks "did I get JSON" will read an error as a result. 3. **Distinguish an empty reply from a timeout.** A closed connection is fast; a wrong port is often a refusal; a program still starting is slow. The shapes differ even when the outcome does not. 4. **Fail loudly on either.** The dangerous ending is a step that swallows the failure and lets the run continue, because everything downstream then reports on a scan that never happened.

  • Which other refusals behave the same silent way?
    A call rejected because the API is disabled, a request from an address that is not on the permitted list, and a plaintext request when `api.secure` requires HTTPS. All are decided before the call is resolved, and all end the connection without a reply.
  • What does `api.incerrordetails` add on top of `api.reportpermerrors`?
    It adds a `detail` field to error documents, naming the offending parameter or condition rather than only the error class. It widens what an unauthenticated caller learns, so it belongs to bring-up on a closed runner rather than to a long-lived instance.
  • How should a step tell "the program is not up yet" from "my key is wrong"?
    Prove liveness first with a call that needs no state, and if permission errors are reported, read the status code: a `400` means you reached the program and it refused you. Failing that, the shapes differ — a closed connection is immediate, a wrong port is usually a refusal, a starting program is slow.

It is the difference between a door that says "wrong key" and a door that simply never opens: the second tells a stranger nothing, and tells you nothing either.

saying these in an interview costs you the question

  • Expects a 401 or 403 for a wrong API key
  • Reads an empty reply as proof the program is not running
  • Treats any parseable JSON body as a successful result
  • Leaves api.reportpermerrors on for a broadly reachable instance
  • Lets the step continue after the call returns nothing