In k6, how do you send a JSON body with http.post(), and what does a plain object send instead?
answer
- encoding follows the body's type
- an object becomes a form, not JSON
- stringify before you post
- Content-Type lives in params.headers
- res.request.body shows what was sent
basics
~10 sk6 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 linesimport 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
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.
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.
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.
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