HTTP, REST and API testing technology
How HTTP really works - methods, status codes, headers, cookies, redirects and TLS - then REST and OpenAPI, the .NET HttpClient stack, how a test engine fills variables and judges JSONPath and JSON Schema checks, how to stop SSRF, mock servers, load-testing math, breaking-change detection and API tests in CI.
In short
An API test is an ordinary HTTP request with a judgement attached. A test engine fills in variables, sends the request, reads the answer and runs checks on it - a status code, a field found by JSONPath, a whole body against a JSON Schema. Around that core sit the hard parts. One is never letting a user-typed address reach your own network (SSRF). Another is measuring speed honestly under load. A third is spotting breaking changes between two OpenAPI files, and failing a CI build with the right exit code. In {{brand}}, one shared run engine builds and judges every request, suite and project run, on the server and in the command line alike.
After this lesson you can Read any HTTP exchange line by line and explain each status, header and redirect; build a small API test runner with variables, JSONPath and JSON Schema checks; defend an outbound .NET HttpClient against SSRF; read load-test percentiles without being fooled by coordinated omission; and wire API tests into a CI pipeline.
Words you need
- HTTP The text protocol of the web: a client sends a request (method, path, headers, body), a server sends a response (status, headers, body).
- Method The verb of a request: GET reads, POST creates, PUT replaces, PATCH changes part, DELETE removes. Some are safe, some idempotent (see below).
- Status code Three digits that sum up the answer: 2xx success, 3xx go elsewhere, 4xx the client's mistake, 5xx the server's.
- Header A name and a value beside the body - Content-Type, Authorization, Retry-After - saying how to read or treat the message.
- TLS The encryption under https. The server proves its name with a certificate, and the client checks it before sending the request.
- REST A style for APIs: resources at URLs, standard methods on them, and no session kept on the server between requests.
- OpenAPI A JSON or YAML file describing an API's operations, parameters and bodies - a contract that machines can read.
- JSONPath A query language for JSON: a path such as $.items[0].price picks one value, and a wildcard in $.items[*].price picks every price.
- JSON Schema A JSON document describing valid JSON - types, required fields, ranges - so a whole body is checked at once.
- SSRF Server-side request forgery: making a server call an address it should never reach, such as the cloud's metadata service.
- Idempotent Sending the request twice leaves the same state as sending it once, so a client may retry it after a timeout.
- Percentile (p95) The latency that 95% of requests beat. The tail - p95, p99 - is what unhappy users feel; the average hides it.
HTTP in depth: one request, start to finish
When a test sends GET https://api.example.com/orders/42, six things happen in order. Knowing them tells you where a slow or failed request lost its time - and which numbers a tool is really showing you.
- 1. Resolve the name DNS turns api.example.com into one or more IP addresses. A cached answer costs nothing; a cold lookup can cost tens of milliseconds.
- 2. Open a connection A TCP handshake to port 443 costs one round trip. HTTP/1.1 keeps the connection open (keep-alive), so the next request skips this step.
- 3. Agree on encryption The TLS handshake: the client names the host it wants (SNI), the server shows its certificate, both agree on keys. TLS 1.3 needs one round trip, TLS 1.2 two.
- 4. Send the request The request line (method, path, version), the Host header and other headers, a blank line, then the body if there is one.
- 5. Wait, then read The server works; the first byte back marks time to first byte. The body then streams in, sized by Content-Length or sent in chunks.
- 6. Reuse or close The connection goes back to a pool for the next request to the same host, until it has been idle too long.
HTTP/2 sends the same meaning in binary frames and runs many requests at once over one connection; HTTP/3 does the same over QUIC on UDP. Methods, status codes and headers are unchanged, so everything below applies to all three. The version is agreed during the TLS handshake through ALPN.
Safe and idempotent, in two sentences Safe means the request changes nothing on the server. Idempotent means sending it twice leaves the same state as sending it once, so a client may retry it after a timeout - POST is neither, which is why a retried POST can create two orders.
Some headers belong to the connection, not the request Host, Content-Length, Connection, Transfer-Encoding and Keep-Alive describe one connection. Copied from a browser and replayed against a different body, they cause hangs or 400 answers that look like the server's fault. A good test client drops them and lets the transport set them.
Cookies, redirects and TLS: where tests quietly go wrong
These three features exist to make browsing pleasant. In a test they can hide what really happened, so a test client handles each one deliberately instead of leaving it to defaults.
A browser keeps one cookie jar for everything. A test tool should keep one jar per run: the session cookie from the login step reaches the steps after it, and nothing leaks between runs of different people. {{brand}} keeps a fresh jar for every suite run and every data row.
A redirect can carry your credentials away Careful clients drop the Authorization header when a redirect leaves the origin, but custom code, cookies on a wide domain and some options (curl --location-trusted) can still carry credentials away. A test client should follow redirects one hop at a time, drop credentials and cookies when the origin (scheme, host and port) changes, record every hop, and check each new address as if it were a new request.
- ClientHello The client offers TLS versions, ciphers and a key share, names the host it wants (SNI) and the protocols it speaks (ALPN: h2, http/1.1).
- ServerHello and certificate The server picks a cipher and sends its key share - from here on everything is encrypted - then its certificate chain.
- Verify The client checks the chain up to a trusted root, the validity dates, and that the certificate names the host it asked for.
- Finished Both sides send a Finished message that proves nobody changed the handshake. The HTTP request goes out right after it.
Turning off certificate checks is a decision, not a fix A test server with a self-signed certificate fails verification. Accept it only for that one test environment, scope the switch to a single environment, never in global settings, and never for production - otherwise anyone between you and the server can read your tokens.
REST and OpenAPI: the shape of an API and its contract
REST comes from Roy Fielding's 2000 dissertation. In practice it means: things are resources with URLs such as /orders/42, the HTTP method says what to do with them, and each request carries everything the server needs - no hidden session state.
OpenAPI (formerly Swagger) writes that interface down. Version 3.1 describes bodies with JSON Schema 2020-12, so the same schema both documents the API and checks a real response. Swagger 2.0 files still exist - they name a host and a base path where 3.x has servers - and good tools read both, in JSON or YAML.
From one file like this a tool can make a request per operation, a schema check per documented answer, a mock that answers from the examples and - comparing two versions - a list of breaking changes. That is why the contract sits at the centre of API testing.
The options in the market
Two decisions shape an API testing tool: how tests are written and run, and which HTTP client sends them. Here are the honest choices for each.
- Postman collections + Newman JavaScript test scripts inside a collection, run from a CLI.; A huge user base and an easy start; Scripts can do anything; Shared history and team features depend on a cloud account and plan; Running users' scripts on a shared server needs a sandbox
- Code first: REST Assured, xUnit + HttpClient, pytest + requests Tests are ordinary code in the repository.; The full power of a language; Reviewed and versioned like code; Only developers can write them; No shared history across a team
- Declarative files: Hurl, Bruno, Karate Requests and asserts in a plain text format, with optional scripting in Bruno and Karate.; Readable and easy to diff; Mostly declarative, so most of a test is readable data; Limited logic; Storing and sharing results is yours to build
- Contract-driven: Schemathesis, Dredd, Pact Tests are generated from an OpenAPI file or a consumer contract.; Finds edge cases nobody wrote; Tests follow the contract; Generated tests need judgment; Not a replacement for business-flow tests
- A declarative engine with stored results Checks are data, judged by one engine on the server and in CI; results are kept for the team.; No user code ever runs on the server; The same verdict everywhere; History shared by the whole team; No escape hatch for exotic logic; The engine itself must be built and maintained
- new HttpClient() for every request A fresh client, and so a fresh connection pool, each time.; Looks simple; Each instance opens its own connections; closed sockets pile up in TIME_WAIT (socket exhaustion)
- One static HttpClient One shared client for the life of the process.; Connections are reused; Never sees DNS changes unless connections are recycled; One configuration for every target
- Wrappers: RestSharp, Flurl, Refit Pleasant APIs and typed clients; Another layer between you and the socket: harder to control redirects, timing and connections
- IHttpClientFactory + SocketsHttpHandler Named clients, each a pipeline of handlers over a configured socket handler.; Pooled, long-lived connections; Settings per client: redirects, cookies, decompression, proxy; Your own DelegatingHandlers and a ConnectCallback for address checks; Resilience handlers (retries, circuit breaker) plug into the same pipeline; More set-up, and more to understand
What {{brand}} chose, and why
{{brand}} wrote its own run engine: a small library that builds requests, fills variables, reads answers with a size cap and judges declarative checks. The server and the command line both use it, so a check cannot pass in one and fail in the other.
- No user code on the server: checks are data - a status, a header, a JSONPath, a schema, a pattern - so there is nothing to sandbox and nothing to escape from.
- Honest measurements: a test client should add no conveniences of its own - no silent redirects, shared cookies or hidden retries - because each one distorts what you measure.
- History for a team: runs from signed-in people are recorded according to the project's storage setting, with secrets masked, so a team can say what was tested, when, by whom and with what result. Load tests keep a summary.
- One checkpoint for outbound calls: with a single place that opens connections, no code path can forget the address check.
The trade-off accepted There is no scripting escape hatch. A test that needs real logic - signing a request with a custom algorithm, say - has to be done another way. The team accepted that, because the alternative is running strangers' code on shared servers.
When another option is right: a team of developers who keep tests next to the code may be happier with code-first tests in xUnit or pytest. One person exploring a new API is quicker in Postman or Bruno. A shared engine pays off when many people, a server and CI pipelines must agree on one verdict.
.NET HttpClient, IHttpClientFactory and message handlers
HttpClient is only a front door. The real work happens in a chain of message handlers: each one may change the request, answer it itself, or pass it on. The last one, SocketsHttpHandler, owns the connection pool and the sockets.
- HttpClient.SendAsync Applies the client's timeout - 100 seconds by default - and hands the request to the first handler.
- DelegatingHandler 1, 2, ... Your own handlers, outermost first: add a header, record timing, answer a fake host in process, or ask a rate gate for permission.
- SocketsHttpHandler Finds a pooled connection for the host or opens a new one - through your ConnectCallback if you set one - then writes the request.
- The answer travels back Up through the same handlers in reverse order, so each can see the status and the headers.
The two classic HttpClient bugs Socket exhaustion: creating and disposing an HttpClient per request leaves closed sockets waiting in TIME_WAIT until the machine runs out of ports. Stale DNS: one HttpClient kept forever keeps talking to an old IP after a fail-over. IHttpClientFactory fixes the first by pooling handlers; PooledConnectionLifetime fixes the second by recycling connections.
Read bodies with HttpCompletionOption.ResponseHeadersRead and a byte cap of your own. By default HttpClient buffers the whole body in memory first, so a huge or endless answer costs that much memory. A test engine reads up to its limit and stops. Remember the consequence: a body cut at the cap is usually invalid JSON, so a JSON check on it fails - raise the cap, or check only headers for very large answers.
Building a test engine: variables, extraction and checks
Most API testing tools run roughly the same pipeline for each request. The details differ; the stages do not.
- Layer the variables Collect values from the widest scope to the narrowest, with the narrowest winning. A typical order: values shared by the whole workspace, the environment, a data row, values extracted from earlier steps, and the request's own variables on top. Dynamic values, such as a random id, are not a layer - they are made on demand.
- Substitute Replace every {{name}} in the address, headers and body. An unknown name stays as written, so the mistake shows in the request instead of becoming an empty string.
- Send and time Send through the careful client; start the clock when the request leaves and stop it when the body has been read.
- Extract Copy values out of the answer - a token by JSONPath, an id from a header - into variables for the next step.
- Judge every check Run all checks even after one fails (soft assertions), so a person sees every problem in one run.
- Mask, then store Replace secrets - tokens, passwords, API keys - with a mask before anything is written down or shown.
Whatever the tool, checks come in a few kinds. A good engine offers each of these and lets one request carry several.
Why a dynamic value is made once per request If {{$uuid}} appears in the address and again in a check, both must see the same value - otherwise the check compares against a different random id. Make it once per request, and a new one for the next request.
The order used above is: status shipped, and two items - A1 at price 10 and B7 at price 25.5. JSONPath was described by Stefan Goessner in 2007 and became a standard, RFC 9535, in 2024; many tools still speak their own dialect. {{brand}} supports a deliberate subset - names, indexes, wildcards and a one-comparison filter. Whatever a tool does not support often raises no error: a path outside its subset can quietly match nothing, or the wrong thing. So test the exact paths you depend on.
A path can reach many items, and then a check must say whether every item must pass or any one is enough. 'Every price is above 0' and 'some item is in stock' are different tests. A good engine checks every item by default for a wildcard and reports how many failed.
A validator reports each failing place as a path, so one schema replaced three hand-written checks. An OpenAPI file already holds a schema for every documented answer. {{brand}} validates with JsonSchema.Net and lists the first places that do not match.
Two traps inside checks ReDoS: a pattern such as ^(a+)+$ can take exponential time on a crafted text, so every regular expression a user supplies must run with a timeout. Remote $ref: a schema that points at an outside URL would make your server download whatever it names - resolve references only inside the schema, and treat anything else as a failed check.
Authentication and other things real API tests meet
Almost every real API needs a credential. The test engine's job is to obtain it once, keep it out of logs and send it on every request that needs it.
- Obtain A first step signs in, or calls the token endpoint, and extracts the token from the answer.
- Send Later steps put it in the Authorization header as a variable, never as pasted text.
- Expire When the API answers 401, the token is old: get a new one with the refresh token or sign in again, then repeat the request once.
- Pagination: follow the next-page link or cursor until it ends, and check that no item appears twice.
- Idempotency keys: send the same key twice and check that only one order is created.
- Rate-limit headers: read the remaining-requests and reset headers a server sends, and slow down before a 429.
SSRF: when users choose the address
An API tester sends requests to addresses its users type. That is exactly the shape of server-side request forgery: an attacker types an address the server can reach but the attacker cannot - an admin page on the private network, or the cloud metadata service at 169.254.169.254 that hands out credentials. SSRF has its own place in the OWASP Top 10 (A10:2021), and a well-known 2019 breach of a large bank started this way.
- A literal private address http://10.0.0.5/admin or http://127.0.0.1:6379 - stopped by a list of private ranges.
- Odd spellings of an IP http://2130706433/ and http://0177.0.0.1/ both mean 127.0.0.1, and [::ffff:127.0.0.1] hides it inside IPv6. Parse the address; never compare text.
- A name that points inside internal.attacker.example has an A record of 10.0.0.5. The URL text shows a public name; only resolving it shows the truth.
- DNS rebinding The name answers a public IP when you check it and a private one a moment later when the client connects. A check and a connection that resolve separately can be told different things.
- A redirect inside A public URL answers 302 to http://169.254.169.254/. A client that follows redirects automatically never checks the second hop.
- A proxy that resolves for you Through an HTTP proxy, the proxy resolves the name - not your guard.
- Allow only http and https Refuse file:, gopher: and every other scheme before anything else.
- Resolve the name yourself Ask DNS once and take every address the name has.
- Refuse if any address is restricted One private address among public ones is enough to refuse - otherwise the client may simply pick it.
- Connect to the address you checked Pin the connection to the vetted IP, so a second DNS answer cannot change the target. TLS still checks the certificate against the host name.
- Follow no redirect automatically Treat each hop as a new request: check it, count it, and drop credentials when the origin changes.
- No proxy, no odd ports Use no proxy for user targets, and refuse ports such as SMTP or SMB that no API test needs.
This list is illustrative, not complete: a production guard also covers reserved, multicast and cloud-platform addresses. It should be one tested, shared helper, not code pasted into a handler. It also does not check ports; refuse the ones no API test needs.
Check at connection time, not at parse time A check on the URL text only catches honest mistakes. The real guard sits where the socket opens, because only that place knows the address actually used - for the first request, every redirect hop and every pooled connection. Public-facing testers should apply the same rule to every outbound call, and bind heavy traffic only to a target whose owner has proved control of it.
Private APIs on localhost are still testable from the person's own machine: a command-line runner or a browser extension sends the request from where it is allowed to go, so a shared server never has to reach into a private network.
Mock servers
- Stub Answers with fixed data and does not check how it was called.
- Mock Answers and also verifies how it was called - which requests, how many times.
- Fake A working but simplified version, such as an in-memory database.
- Mock server A stub over real HTTP (also called service virtualization), so any client - a browser, a phone app, a test - can call it.
- Contract test Proves that a consumer and a provider agree, for example with Pact: the consumer's expectations are replayed against the real provider.
- Match method and path GET /orders/42 matches a saved GET /orders/{id}; a HEAD is answered like a GET.
- Prefer the most specific A path written out, /orders/latest, beats /orders/{id}. Then a route whose fixed query values all appear in the call.
- Choose the example The caller may ask for a named example or a status - give me the 404 - otherwise the default example answers.
- Shape the answer A simulated delay or error rate tests timeouts and retries before the real API exists.
A public mock is a public web page Anything a mock serves to a browser can run there. A safe mock serves only inert types such as JSON and plain text, sends nosniff and a sandboxing Content-Security-Policy, never sets cookies, and keeps redirects inside itself - so it cannot become a phishing page on your domain.
Load testing: the math behind the numbers
- Concurrency How many requests are in flight at once - roughly the number of virtual users.
- Throughput Requests completed per second (RPS) - the work rate.
- Latency How long one request takes, from sending to the last byte of the answer, usually reported as a percentile.
- Error rate The share of answers that are errors or timeouts. Fast errors make latency look great.
They are tied together by Little's law: concurrency = throughput x latency. With 50 requests in flight taking 200 ms each, the most you can complete is 50 / 0.2 = 250 requests a second. If latency doubles to 400 ms, throughput halves to 125 - the server did not get busier, it got slower.
Three rules for reading percentiles Never average percentiles across runs - merge the raw samples instead. You need well over 100 samples before p99 means anything. And the tail compounds: a page that calls 100 services, each with a 1% chance of a slow answer, is slow for about 63% of visitors (1 - 0.99 to the power 100).
{{brand}} computes percentiles by nearest rank with no interpolation, so a reported p95 is always a duration that really happened. It stores the distribution - p50, p90, p95 and p99, a latency histogram, status counts and a per-second timeline - not every single response, and compares a run with a baseline on p95, throughput and error rate.
- Closed model A fixed number of workers; each sends, waits for the answer, then sends again.; Simple and safe: load can never exceed the number of workers; Matches clients that wait, such as a connection pool; When the server stalls, workers stop sending, so the requests that would have queued are never measured - coordinated omission
- Open model Requests arrive at a fixed rate, whether or not earlier ones have finished.; Measures from the intended send time, so queueing shows; Matches real users, who do not wait for each other; Can pile thousands of requests onto a struggling server; Needs care to stay polite
Coordinated omission, a name coined by Gil Tene, is the blind spot of a generator that waits: when the server stalls, it stops sending, so the stall is never measured. Example: one worker should send every 100 ms for 100 seconds, and the server freezes once for 2 seconds.
Same server - a hundred times apart. Pick the model on purpose: a closed model answers 'how long does a request take with N in flight', an open model answers 'what do users see at R requests a second'.
- k6 with arrival-rate executors
- Gatling
- wrk2
- Ramp up Start workers one by one over a ramp-up period, so caches warm and an autoscaler can react.
- Respect 429 On 429 Too Many Requests never retry at once: read Retry-After and pause every worker, not only the one that was told.
- Retry little, with backoff Retry only timeouts, 408 and 5xx answers, a few times, with a growing pause - and keep a retry budget for the whole run so retries cannot double the load.
- Stop when the target struggles When most recent answers are server errors or timeouts, stop. A test that knocks a service over has measured nothing useful.
- Prove ownership for heavy runs A tool that points heavy load at any site is a denial-of-service tool. Responsible tools ask the owner to prove control of the site before they allow heavy runs.
Spotting breaking changes between two OpenAPI files
A breaking change makes a working client fail. Comparing two versions of the contract catches most of them before a deploy. The rule of thumb is Postel's robustness principle: a change is safe when the server accepts at least what it accepted before and promises at least what it promised before - in type language, contravariant inputs and covariant outputs.
- Read both files Parse OpenAPI 3.x or Swagger 2.0, in JSON or YAML, without fetching anything the file points to.
- Normalize the paths /pets/{id} and /pets/{petId} are the same operation: every path parameter becomes one placeholder.
- Pair the operations By method and path; what has no partner is an added or a removed endpoint.
- Run each rule One small rule per kind of change walks both schemas side by side - request and response separately, because what is safe for one breaks the other.
- The verdict is the worst finding Any breaking finding makes the version Breaking; else any doubtful one makes it Needs review; else it is Safe.
Request and response are mirror images Widening is safe in a request and dangerous in a response; narrowing is the opposite. If you remember one thing about API compatibility, remember this.
Open-source tools such as oasdiff and openapi-diff apply the same idea in CI. {{brand}} runs its own rules - reading files with Microsoft.OpenApi - so that a breaking change can be linked to the tests and the people it affects, and wait until someone verifies it.
Running API tests in CI
- Install a pinned tool Install the runner at an exact version from one known feed - a moving 'latest' is how a supply-chain attack enters a pipeline.
- Secrets from the environment The token and the API's secrets come from secret variables as environment variables - never as command arguments, which process lists and logs show.
- Say where the run may send Name the hosts the run may reach; anything else is refused before it leaves.
- Run and write JUnit The runner writes a JUnit XML file: one test case per step, with failures that say what was expected and found.
- Publish, even on failure Publish the results in a step that runs always, so a red build still shows which test failed.
- Let the exit code decide The process exit code turns the build red or green.
Telling tests failed (1) from could not run (2) matters: the first is a bug in the API, the second a problem with the pipeline. A runner that returns 1 for both sends people to debug the wrong thing.
JUnit XML started with the Java tool Ant and became the format every CI system reads: Azure Pipelines, GitHub Actions, GitLab and Jenkins. A failure means an assertion did not hold; an error means the test could not run. {{brand}}'s CLI writes one test suite per data row and reports a step that was not sent as an error, never as a pass.
Pipeline logs read commands Azure Pipelines treats a log line containing ##vso[ as a command, and GitHub Actions treats lines starting with :: the same way. A test runner prints names and values other people wrote - a step name, a response field - so it must escape them, or a hostile API answer could set pipeline variables.
Try it yourself: a tiny API test runner
Build the core of a test engine against a local fake API. You need Node.js 20 or newer and nothing else installed; npx fetches json-server and tsx on the fly.
- Start a fake API Save db.json below and run npx [email protected] db.json (json-server 1.x works too) - a REST API on http://localhost:3000 with GET /posts and GET /posts/1.
- Describe two steps Step 1 lists posts and extracts the first id; step 2 reads that post and checks its id - a real chain.
- Run the runner Save the code as runner.ts and run npx tsx runner.ts. It substitutes variables, sends with redirect: 'manual', times each call and judges the checks. Expect two lines like PASS List posts (200, 12 ms) and PASS Read the first post (200, 4 ms).
- Read the JUnit file Open results.xml - exactly what a CI system reads.
- Break it on purpose Change an expected status to 201 and run again: one FAIL line, a failure in the XML and exit code 1 (echo $? on Linux or macOS).
- Stretch goals Add a JSON Schema check (npm i ajv, then new Ajv().compile(schema)(body)), a response-time check, and the retry with backoff from the load-testing section.
Common mistakes
- Following redirects blindly: an automatic client resends credentials to another host and hides the hop from the result. Follow hops yourself, one at a time, and record them.
- Checking the URL instead of the connection: a text check misses DNS tricks and rebinding. Resolve, check every address, and connect to the one you checked.
- Trusting the average: a 118 ms mean can hide a 980 ms p99. Report percentiles, from enough samples to trust them.
- Retrying everything: a retried POST can create duplicates, and retrying a 429 at once makes the overload worse. Retry only idempotent requests and transient errors, with backoff and a budget.
- Stopping at the first failed check: people then fix one problem per run. Judge every check and report them all.
- One exit code for every failure: CI cannot tell a broken API from a broken pipeline. Keep 'tests failed' and 'could not run' apart.
Interview questions
- What is the difference between PUT and PATCH, and which is idempotent? PUT replaces the whole resource at a known URL, so repeating it leaves the same state: it is idempotent. PATCH changes part of it, and whether a repeat is harmless depends on the patch - 'add 1' is not - so it is not idempotent in general.
- What is the difference between 401 and 403? A 401 means the server does not know who you are (missing or bad credentials). A 403 means it knows you, and you are not allowed. A 404 is sometimes used instead of 403 so that existence is not revealed.
- How do you make a POST safe to retry? Send an idempotency key header; the server stores the first result under it and returns the same answer for repeats, so a timeout retry cannot create two orders.
- What is the difference between a 302, a 303 and a 307? A 303 always makes the next request a GET without the body. A 307 repeats the same method and body. A 302 is officially like 307, but clients historically turned a POST into a GET, which is why 303 and 307 were added to be unambiguous.
- Why should you not create a new HttpClient for every request in .NET? Each instance owns a handler with its own connection pool; disposing them leaves sockets in TIME_WAIT and can exhaust ports under load. Use IHttpClientFactory, or one long-lived SocketsHttpHandler with PooledConnectionLifetime so connections are reused and DNS is still refreshed.
- How do you prevent SSRF in a service that calls user-supplied URLs? Allow only http and https, resolve the host yourself, and refuse if any address is loopback, private, link-local or metadata. Connect to the checked address so DNS cannot change in between, follow no redirect automatically, use no proxy, and add an egress firewall as a second line.
- What does p99 tell you that the average does not? That 1 request in 100 is slower than this value - the tail users actually feel. The mean mixes fast and slow and describes nobody; percentiles need enough samples and cannot be averaged across runs.
- What is coordinated omission? A closed-loop load generator waits for slow answers, so during a stall it stops sending and never measures the requests that would have queued. Latency then looks far better than real users see; open-model tools measure from the intended send time to correct it.
- How do you decide whether an OpenAPI change is breaking? Pair operations by method and normalized path, then compare request and response schemas with opposite rules: a request may only accept more, a response may only promise the same or more. Removed endpoints, new required inputs, narrowed request types and removed response fields break callers.
- How should an API test runner report to CI? Write JUnit XML so the CI shows each test case, publish it even when the run failed, and use distinct exit codes - 0 passed, 1 tests failed, 2 could not run. Read secrets from environment variables and escape everything printed to the log.
Related
- /help/technology/testing
- /help/technology/browser-automation
- /help/technology/azure-devops