skip to content

In k6, how do you send a JSON body with http.post(), and what does a plain object send instead?

level: juniorimportance: must knowfreq 76%

answer

  1. encoding follows the body's type
  2. an object becomes a form, not JSON
  3. stringify before you post
  4. Content-Type lives in params.headers
  5. res.request.body shows what was sent

basics

~10 s

k6 picks the encoding from the body's JavaScript type: a plain object is form-encoded as application/x-www-form-urlencoded. For JSON, pass JSON.stringify(payload) as the body and set Content-Type in the params headers object.

solid answer

~40 s

`http.post(url, body, params)` chooses the wire format from the **type** of `body`, not from the headers you set. A JavaScript string is sent byte-for-byte; a plain object is serialised to `application/x-www-form-urlencoded` and k6 sets that `Content-Type` for you; an `ArrayBuffer` is sent as raw bytes. So a JSON POST is two moves: `JSON.stringify(payload)` for the body, and `{ headers: { 'Content-Type': 'application/json' } }` as the third argument. Declaring `application/json` while still passing the object only relabels a form-encoded body -- k6 does not re-serialise it, and the server sees `a=a&b=2` under a JSON label. `res.request.body` shows exactly what went on the wire.

code

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

export default function () {
  const url = 'https://quickpizza.grafana.com/api/post';
  const payload = JSON.stringify({ sku: 'A-19', qty: 3 });
  const params = { headers: { 'Content-Type': 'application/json' } };

  const res = http.post(url, payload, params);
  console.log(res.status, res.request.body);
}

go deeper

for a junior

Memorise the three-argument shape http.post(url, body, params) and the rule that a plain object body becomes a URL-encoded form. For JSON, stringify the payload and put Content-Type inside the params headers object.

for a middle

Be able to explain that k6 selects the encoding from the body's JavaScript type before headers are applied, and to name what each type produces: string verbatim, object form-encoded, object with an open() value multipart, ArrayBuffer raw bytes.

for a senior

Show how you would catch the relabelled-body bug in review or in a smoke run: read res.request.body, watch for a 400 that only the load test provokes, and use httpDebug when the client and server disagree about a payload.

for a principal

Decide what the team standardises: a shared helper that pairs JSON.stringify with the Content-Type header, or raw http.post calls everywhere. The helper removes a whole class of silent encoding bugs at the cost of one more indirection to read.

## The call surface `k6/http` exposes one function per verb -- `http.get`, `http.post`, `http.put`, `http.patch`, `http.del`, `http.head`, `http.options` -- plus the generic `http.request(method, url, body, params)` and its promise-returning twin `http.asyncRequest(method, url, body, params)`. The verb helpers are thin wrappers over `request`: `http.post(url, body, params)` is exactly `http.request('POST', url, body, params)`. Note the spelling `http.del`, not `http.delete`; `delete` is a reserved word in JavaScript. `http.get` and `http.head` take no body argument at all -- they are `(url, params)`. ## k6 chooses the encoding from the body's type This is the single fact that trips people up. k6 inspects the JavaScript type of the `body` argument and encodes accordingly. The `headers` you supply are applied on top and never change the encoding. | `body` you pass | What goes on the wire | `Content-Type` k6 sets | |---|---|---| | a string | the string, byte for byte | none (yours, or nothing) | | a plain object | `a=a&b=2` | `application/x-www-form-urlencoded` | | an object holding a value from `open(file, 'b')` | a multipart body with a boundary | `multipart/form-data; boundary=...` | | an `ArrayBuffer` | the raw bytes | none (yours, or nothing) | So `http.post(url, { a: 'a', b: 2 })` puts `a=a&b=2` on the wire. Array values become repeated keys: `{ c: ['one', 'two'] }` becomes `c=one&c=two`. Nested objects cannot be represented in a flat form body at all -- k6 encodes them as an empty value and logs a warning telling you to use `JSON.stringify()`. ## Sending a JSON POST with explicit headers Two things are required, and neither is optional: 1. **Serialise it yourself.** `JSON.stringify(payload)` turns the object into a string, and strings are sent verbatim. 2. **Declare the type yourself.** Put `Content-Type` in the `headers` object of the third argument, because k6 only sets a `Content-Type` for the object and multipart cases. ```javascript const payload = JSON.stringify({ sku: 'A-19', qty: 3 }); const params = { headers: { 'Content-Type': 'application/json' } }; const res = http.post('https://api.example.com/orders', payload, params); ``` ## The failure mode: relabelling without repacking Setting `Content-Type: application/json` while still passing the object is the classic bug. k6 form-encodes the object as usual, then your header overwrites the one k6 set. The request leaves as `a=a&b=2` announced as JSON, and the server answers `400` on a body it cannot parse. Nothing in k6 warns you, because from k6's side both halves did exactly what they were asked. Two habits catch it: - Read `res.request.body` back in a console log during development -- it holds the literal body k6 sent. - Treat `JSON.stringify` as part of the same edit as the `Content-Type` header; they are never independent. ## The params object The third argument is a plain object; k6 recognises a fixed set of keys and **silently ignores anything else**, so a typo such as `header:` or `timeOut:` costs you the setting with no error. - `headers` -- key/value pairs merged into the request. A `Host` entry sets the request host. - `cookies` -- request-scoped cookies that are not added to the VU cookie jar. - `tags` -- extra metric tags for the samples this request produces. - `timeout` -- `'30s'` as a string, or a plain number read as milliseconds; the default is 60 seconds. - `redirects` -- how many redirects to follow for this request, overriding the `maxRedirects` option. - `responseType` -- `'text'`, `'binary'` or `'none'`, controlling what `res.body` holds. - `responseCallback` -- an `http.expectedStatuses(...)` object scoped to this one request. - `compression`, `jar`, `auth` -- body compression, an explicit cookie jar, and the auth scheme. One more measured detail: a `Content-Length` header you set by hand is **deleted** and recomputed from the actual body, with a warning logged if the two disagree. ## Reading the response back The call returns a `Response` object synchronously. `res.status` is the numeric status, `res.status_text` the full status line text, `res.body` the body as a string by default, and `res.headers` the response headers. `res.json()` parses the body and caches the result, so calling it repeatedly is cheap; it also takes an optional selector string -- `res.json('data.items.0.id')` -- using gjson path syntax. `res.timings` carries the per-phase float durations in milliseconds, and `res.request` echoes the `method`, `url`, `body`, `headers` and `cookies` that k6 actually sent. All of this is k6 v2.x behaviour.

  • How would you upload a file alongside form fields with http.post() in k6?
    Build an object body whose values mix plain fields with a value returned by `open(path, 'b')` in the init context. k6 detects the file value, switches the body to `multipart/form-data`, and sets the boundary `Content-Type` itself. Do not set `Content-Type` by hand there -- your header would replace the generated boundary and the server could not split the parts.
  • What is the difference between http.post() and http.asyncRequest('POST', ...) in k6?
    `http.post()` blocks the VU until the response arrives and returns a `Response`. `http.asyncRequest('POST', url, body, params)` takes the same arguments, runs the networking off the event loop, and returns a Promise that resolves to the same `Response`, so you can `await` it or start several and await them together inside one iteration.
  • In k6, how do you confirm what body actually left the client?
    Read `res.request.body`, which holds the literal request body k6 sent, alongside `res.request.method`, `res.request.url` and `res.request.headers`. For a whole-run view, the `httpDebug` option (`--http-debug` or `--http-debug=full`) logs requests and responses, with `full` including bodies.

Setting Content-Type to application/json on an object body is relabelling a parcel without repacking it. The label changed; the contents did not.

saying these in an interview costs you the question

  • Believing a Content-Type header makes k6 serialise the body as JSON
  • Passing a plain object and expecting a JSON request body
  • Calling http.delete() instead of http.del()
  • Putting headers as the second argument of http.post()
  • Assuming an unrecognised params key raises an error rather than being ignored
  • Setting Content-Length by hand and expecting k6 to send it