skip to content

With discardResponseBodies enabled in k6, what is res.body, and how do you keep one response's body?

level: seniorimportance: nice to knowfreq 44%

answer

  1. the option moves a default, not a switch
  2. responseType becomes none
  3. body comes back null
  4. json() then has nothing to parse
  5. responseType in params opts back in

basics

~10 s

The option flips every request's default responseType from text to none, so res.body comes back null and res.json() throws. Keep one body by passing responseType 'text' or 'binary' in that request's params object.

solid answer

~40 s

`discardResponseBodies: true` does not turn reading off -- it changes the **default** value of each request's `responseType` from `'text'` to `'none'`. k6 still reads the response to the end so timings stay honest, then throws the bytes away and sets `res.body` to `null`. Any script code that touches the body afterwards fails: `res.json()` raises `the body is null so we can't transform it to JSON`. To keep a particular response, set `responseType` in that request's params -- `'text'` gives you a string, `'binary'` gives you an `ArrayBuffer`. The same key works the other way round: with the option off, `responseType: 'none'` discards one specific body. It is a per-request override of a script-wide default, not an all-or-nothing switch.

code

javascript · 15 lines
javascript
import http from 'k6/http';

export const options = { discardResponseBodies: true };

export default function () {
  // Bodies of these are discarded; status and timings are still recorded.
  http.batch(['https://quickpizza.grafana.com/', 'https://quickpizza.grafana.com/api/get']);

  // This one is kept, because its params ask for it.
  const res = http.post('https://quickpizza.grafana.com/api/post', JSON.stringify({ sku: 'A-19' }), {
    headers: { 'Content-Type': 'application/json' },
    responseType: 'text',
  });
  console.log(res.json('sku'));
}

go deeper

for a junior

Know that discardResponseBodies makes res.body null, so anything that parses the body stops working. The fix is a responseType entry in that one request's params object.

for a middle

Explain that the option only changes the default responseType from text to none, that k6 still reads the response off the wire, and that status, headers and timings are untouched.

for a senior

Show the working pattern for a large script: discard by default, opt back in per request, and recognise the misleading null-body JSON error for what it is rather than chasing a phantom request failure.

for a principal

Weigh a discard-by-default policy against the review cost it creates. Every future assertion on a body needs a matching override, so the saving is real but it moves a class of failure from load time to script-authoring time.

## What the option actually changes `discardResponseBodies` is a root option in the exported `options` object, and its effect on `k6/http` is narrower than the name suggests. It does not stop k6 reading the response. It changes the **default** value of the per-request `responseType` parameter from `'text'` to `'none'`. k6 still consumes the response body to the end of the stream -- it has to, or the connection could not be reused and the receive phase could not be measured. What changes is what happens next: instead of decoding the bytes into a JavaScript string and handing them to the script, k6 discards them and sets `res.body` to `null`. ## The three responseType values | `responseType` | `res.body` holds | When it is the default | |---|---|---| | `'text'` | the body as a JavaScript string | when `discardResponseBodies` is off or unset | | `'binary'` | an `ArrayBuffer` of the raw bytes | never; always explicit | | `'none'` | `null` | when `discardResponseBodies` is `true` | `'binary'` exists because the conversion into JavaScript's UTF-16 strings mangles binary payloads. If a request returns an image, a protobuf or a zip, `'text'` will corrupt it and `'binary'` will not; you then wrap the `ArrayBuffer` in a typed-array view such as `new Uint8Array(res.body)` to read it. ## What breaks when the body is gone Everything downstream of `res.body` stops working, and the errors do not name the option: - `res.json()` throws `the body is null so we can't transform it to JSON - this likely was because of a request error getting the response`. The message blames a request error, which is misleading when the real cause is the discard setting. - `res.html()` throws for the same reason. - Any assertion that inspects body text silently has nothing to inspect. - `res.status`, `res.status_text`, `res.headers`, `res.timings`, `res.error` and `res.error_code` are all **unaffected** -- they never came from the body. That last point is the reason the option is usable at all: the vast majority of a load test's assertions look at the status and the timings, not at the payload. ## Opting one request back in The override is a params key on the individual request, and it works in both directions. ```javascript export const options = { discardResponseBodies: true }; export default function () { http.get('https://quickpizza.grafana.com/api/get'); // body is null const r = http.get('https://quickpizza.grafana.com/api/get', { responseType: 'text', // body is a string }); const id = r.json('id'); } ``` The same `responseType` key is accepted in every entry of an `http.batch()` call, so a batch can discard fifteen asset bodies and keep the one API reply it asserts on. An unrecognised value is rejected outright rather than falling back to a default. ## Where the setting can and cannot live The two halves of this mechanism sit at different levels, and mixing them up is the usual source of confusion. - `discardResponseBodies` is a **root option**. It belongs in the exported `options` object, or comes from `--discard-response-bodies` or `K6_DISCARD_RESPONSE_BODIES`. It is read once, before the run starts. - `responseType` is a **request param**. It belongs in the third argument of `http.post()`, the fourth slot of a positional `http.batch()` entry, or the `params` key of an object batch entry. - There is no way to change the root option from inside an iteration; a script that wants a different default mid-run does not exist, and the per-request override is the supported answer. - The override is genuinely per request, not per URL or per hop, so two calls to the same endpoint in one iteration can differ. A related trap: because the option changes only a default, a request that already carries an explicit `responseType` is completely unaffected by flipping it. Reviewing a script for the effect of the option therefore means looking for requests that **lack** the key, not for ones that have it. ## How to decide where to put it 1. Start with `discardResponseBodies: true` at the top of the script, so the expensive default is the safe one. 2. Add `responseType: 'text'` to exactly the requests whose bodies your assertions or your data extraction actually read. 3. Use `'binary'` rather than `'text'` for any non-text payload you must keep intact. 4. Re-check after adding assertions -- a new check on a body added months later will fail with the misleading null-body message until the matching `responseType` is added. The mirror case is just as useful: with `discardResponseBodies` left at its default of `false`, a single `responseType: 'none'` on the one request that returns a large asset avoids materialising that body without changing anything else in the script. All of this is k6 v2.x behaviour.

  • In k6, why use responseType 'binary' rather than 'text' for a non-text payload?
    `'text'` decodes the bytes into a JavaScript string, and that conversion corrupts data that is not valid text. `'binary'` hands back an `ArrayBuffer` of the exact bytes, which you read through a view such as `new Uint8Array(res.body)`. It is also what you pass back as a request body when re-uploading the same bytes.
  • A k6 script fails with a null-body JSON error but the endpoint clearly returned JSON. What do you check?
    Two candidates produce a null body. Either `discardResponseBodies: true` is set in `options` and this request lacks a `responseType` override, or the request never completed -- check `res.status` for `0` and read `res.error_code`. The error message names only the second cause, so rule out the first by reading the options block.

saying these in an interview costs you the question

  • Thinking discardResponseBodies stops k6 reading the response at all
  • Believing the option is all-or-nothing with no per-request override
  • Expecting res.status or res.timings to be lost along with the body
  • Using responseType text for binary payloads and losing bytes
  • Reading the null-body JSON error as proof the request failed