skip to content

What do ZAP's generated API client libraries cover, and which part of the control API do they miss?

level: seniorimportance: nice to knowfreq 30%

answer

  1. nobody writes these libraries by hand
  2. one method per call, one module per component
  3. one request type has no method
  4. the failure path is the caller's problem

basics

~20 s

They are generated from the API components a build registers, one module per component and one method per call, covering view, action and other endpoints. Persistent-connection endpoints get no generated method and must be called by hand.

solid answer

~40 s

The clients are generated, not hand-written: core holds a generator per language that walks the registered API components and emits a module or class per component prefix with one method per call. They cover `view`, `action` and `other` endpoints — **not `pconn`**, so a streaming endpoint has to be called with your own HTTP library. Three behaviours matter in a pipeline. The key is set on the client and sent as a header; the `apikey` argument in a generated Python method's signature is accepted and never sent. The client refuses URLs outside the program's reserved host so the key cannot leak to the site under test. And status-code validation is off by default, so a `400` error document comes back as though it were a result.

go deeper

for a junior

Know that these libraries are generated from the program itself, so method names and parameters mirror the API calls. The key goes on the client when you create it, not on each call.

for a middle

Explain what the generators walk and what they skip, and why a client is a snapshot of one build's component set. Be able to say what happens when a call exists in the program but not in your client.

for a senior

Do not trust the happy path: status validation is off by default, so an error document is returned as a result. Make the step check the status and fail rather than storing whatever came back.

for a principal

Decide whether the team standardises on the generated client or on plain HTTP, knowing the client pins you to one build's surface while raw requests spread knowledge of the paths across every script.

## The clients are output, not a product ZAP does not maintain its client libraries by hand. Core contains a generator per target language, all descending from one base class, and each one walks the API components registered in a running build and writes out source: a module or class per component prefix, and one method per call inside it, named after the call and carrying its documented parameters. The description you read in a generated method's docstring is the same message string the program uses for its own documentation. That has a pleasant consequence and an unpleasant one. **Pleasant:** the client cannot drift from the surface in the ordinary way hand-written SDKs do. If a call exists in the build the client was generated from, the method exists, with the right parameter names in the right order. **Unpleasant:** the client is a *snapshot of one build's component set*. Components are contributed by whatever add-ons are installed, so a call that an add-on adds exists in your program and not in your client. When that happens, nothing is broken — you compose the path yourself, because the surface underneath is just HTTP. ## What the generators do not emit The generators walk views, actions and `other` endpoints. **They do not walk persistent connections.** There is no generated method for a `pconn` endpoint in any of the clients, so a streaming endpoint — one that holds the connection open and pushes events as they occur — has to be called by hand with whatever HTTP library you already have. This is the practical answer to "is the client library complete?": it covers three of the four request types, and the missing one is the only one whose responses are not a single document. | request type | generated method? | what you do instead | |---|---|---| | `view` | yes | call the method | | `action` | yes | call the method | | `other` | yes, returning raw text rather than a parsed document | call the method | | `pconn` | **no** | build the path yourself and read the stream | ## Three behaviours worth knowing before you trust one **The key is set once, on the client, and sent as a header.** The Python client's generated methods for actions and `other` endpoints carry an `apikey` parameter in their signature, and that parameter is accepted and never sent. The generator adds it because it has no way of knowing whether a given call will need one; the value that actually authenticates the request is the one passed to the client when it is constructed, which goes out as the header on every call. Passing a key to the method and not to the constructor produces an unauthenticated request, and by the rules of this surface that request is refused in silence. **The client refuses to send the key anywhere but the API.** The Python client rejects any URL that is not under the program's reserved host, with a comment saying it must never leak the key through a proxied request. The separate helper used to make an ordinary proxied request deliberately does not attach the key at all. That is a deliberate guard: the client talks to the target *through* the program, so without it a mistyped URL would send the credential to the site under test. **Status codes are not checked by default.** The Python client takes a flag for validating the response status, and it defaults to off. With it off, a `400` carrying `{"code": …, "message": …}` is parsed and handed back as though it were the result — for a method that unwraps a single-value response, what the caller receives is one of the error object's own fields where a scan identifier was expected. A step that stores that value and carries on will look successful and will be operating on nonsense. ## Choosing between the client and raw HTTP For a pipeline step, the two options trade different things: - **the generated client** gives you named methods, the documented parameter names at the call site, one place to put the credential, and a guard that stops that credential leaving for anywhere but the API; - **raw HTTP** gives you coverage of the whole surface, including the request type the generators skip and any component your client's build did not have, with no dependency to keep in step; - **either way** the response handling is yours: neither shape checks, on your behalf, that what came back is a result rather than an error document. The decision that matters more than either is what the step does with a failure. A generated client with status validation off and a `try` around the call will report success on an error document; a hand-built request that checks the status will not. Neither choice is safe by default — the check has to be yours.

  • Why might a call exist in your ZAP instance but not in your client library?
    Because the client is a snapshot of the component set in the build it was generated from, and components are contributed by installed add-ons. Nothing is broken when that happens — the surface is plain HTTP, so you compose the path yourself.
  • A step passes the key to a generated Python method's `apikey` argument and the call comes back empty. What happened?
    That argument is accepted and never sent; the key that authenticates a request is the one given to the client when it is constructed, which goes out as a header. Without it the request is unauthenticated, and an unauthenticated call is refused silently rather than with a status code.
  • What does the client's refusal to accept non-API URLs protect against?
    Leaking the key. The client sends its requests through the program, so a mistyped or attacker-influenced URL would otherwise carry the credential header to the site under test. The separate helper for ordinary proxied requests deliberately attaches no key.

saying these in an interview costs you the question

  • Believes the client libraries are maintained by hand
  • Assumes a generated client covers every request type
  • Expects the generated apikey argument to authenticate the call
  • Trusts a returned value without checking the status code
  • Treats a missing client method as proof the call does not exist